├── templates ├── template-universal-js │ ├── template │ │ ├── Procfile │ │ ├── src │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ ├── locales │ │ │ │ ├── en.json │ │ │ │ ├── es.json │ │ │ │ └── index.js │ │ │ ├── static │ │ │ │ └── favicon.ico │ │ │ ├── store │ │ │ │ ├── reducers.js │ │ │ │ └── slices │ │ │ │ │ └── user.js │ │ │ └── routes │ │ │ │ ├── Home │ │ │ │ └── Home.jsx │ │ │ │ └── index.jsx │ │ ├── README.md │ │ ├── prettierignore │ │ ├── gitignore │ │ ├── prettierrc │ │ └── eslint.config.mjs │ ├── README.md │ ├── package.json │ └── template.json └── template-universal-ts │ ├── template │ ├── Procfile │ ├── src │ │ ├── styles │ │ │ └── index.css │ │ ├── locales │ │ │ ├── en.json │ │ │ ├── es.json │ │ │ └── index.ts │ │ ├── static │ │ │ └── favicon.ico │ │ ├── routes │ │ │ ├── Home │ │ │ │ └── Home.tsx │ │ │ └── index.tsx │ │ └── store │ │ │ ├── reducers.ts │ │ │ └── slices │ │ │ └── user.ts │ ├── README.md │ ├── gitignore │ ├── prettierignore │ ├── prettierrc │ ├── globals.d.ts │ ├── tsconfig.json │ └── eslint.config.mjs │ ├── README.md │ ├── package.json │ └── template.json ├── universal-scripts ├── runtime.conf.d │ ├── client │ │ ├── 22-css.ts │ │ ├── 70-router.tsx │ │ ├── 50-render.tsx │ │ ├── 30-redux.tsx │ │ └── 33-lang.tsx │ └── server │ │ ├── 70-router.tsx │ │ ├── 07-static.tsx │ │ ├── 33-lang.tsx │ │ ├── 03-custom-error.ts │ │ ├── 50-render.tsx │ │ ├── 30-redux.tsx │ │ └── 20-html-page.tsx ├── lib │ ├── vars │ │ ├── getEnv.js │ │ └── EnvPlugin.js │ ├── redux │ │ ├── actions.ts │ │ ├── lang.ts │ │ ├── selector.ts │ │ ├── types.ts │ │ ├── slices.ts │ │ └── store.ts │ ├── plugins │ │ └── trigger.js │ ├── render-html-layout.js │ ├── universal-config.js │ ├── find-scripts.js │ └── builder.js ├── docs │ ├── _config.yml │ ├── migration.md │ ├── data-fetching.md │ ├── deploying.md │ ├── runtime-system.md │ ├── assets │ │ └── css │ │ │ └── style.scss │ ├── store.md │ ├── index.md │ ├── _layouts │ │ └── default.html │ ├── getting-started.md │ └── build-system.md ├── .npmignore ├── globals.d.ts ├── index.mjs ├── index.cjs ├── scripts │ ├── start.js │ ├── plugin.js │ ├── test.js │ └── build.js ├── build.conf.d │ ├── 11-externals.js │ ├── 12-globals.js │ ├── 15-optimize.js │ ├── 10-entrypoints.js │ ├── 95-gen-index.js │ ├── 30-css.js │ └── 05-base.js ├── server │ ├── serverMiddleware.js │ └── main.js ├── bin │ ├── run-tests.sh │ └── universal-scripts.js ├── index.d.ts ├── client │ └── init.js ├── @types │ └── express │ │ └── index.d.ts ├── config.js ├── package.json └── README.md ├── universal-plugins ├── universal-plugin-jest │ ├── lib │ │ ├── styleMock.js │ │ ├── fileMock.js │ │ └── swcTransform.js │ ├── package.json │ ├── build.conf.d │ │ └── 05-base-test.js │ └── README.md ├── universal-plugin-ssg │ ├── build.conf.d │ │ └── 50-ssg.js │ ├── package.json │ ├── scripts │ │ └── ssg.js │ ├── runtime.conf.d │ │ └── server │ │ │ └── 40-ssg.ts │ └── README.md ├── universal-plugin-helmet │ ├── runtime.conf.d │ │ ├── client │ │ │ └── 19-helmet.tsx │ │ └── server │ │ │ └── 19-helmet.tsx │ ├── lib │ │ └── headers.tsx │ ├── package.json │ └── README.md └── universal-plugin-sass │ ├── package.json │ ├── build.conf.d │ └── 32-sass.js │ ├── README.md │ └── yarn.lock ├── .prettierrc ├── .gitignore ├── package.json ├── tsconfig.json ├── LICENSE ├── eslint.config.mjs └── README.md /templates/template-universal-js/template/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run serve 2 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run serve 2 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/client/22-css.ts: -------------------------------------------------------------------------------- 1 | import 'src/styles' 2 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-jest/lib/styleMock.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-jest/lib/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub' 2 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/styles/index.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/styles/index.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "home.welcome": "Hello, world!" 3 | } 4 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "home.welcome": "Hola, mundo!" 3 | } 4 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "home.welcome": "Hello, world!" 3 | } 4 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "home.welcome": "Hola, mundo!" 3 | } 4 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/README.md: -------------------------------------------------------------------------------- 1 | This project uses [Universal Scripts](https://github.com/GlueDigital/universal-scripts). 2 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/README.md: -------------------------------------------------------------------------------- 1 | This project uses [Universal Scripts](https://github.com/GlueDigital/universal-scripts). 2 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | package.json 5 | yarn.lock 6 | package-lock.json 7 | .eslintrc -------------------------------------------------------------------------------- /universal-scripts/lib/vars/getEnv.js: -------------------------------------------------------------------------------- 1 | export const getEnvVariablesKeys = () => 2 | Object.keys(process.env).filter((key) => key.startsWith('PUBLIC_')) 3 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | .DS_Store 4 | .env 5 | 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | .DS_Store 4 | .env 5 | 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "printWidth": 80, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | package.json 5 | yarn.lock 6 | package-lock.json 7 | .eslintrc 8 | tsconfig.json -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/locales/index.js: -------------------------------------------------------------------------------- 1 | import en from './en.json' 2 | import es from './es.json' 3 | 4 | export default { 5 | en, 6 | es 7 | } 8 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en.json' 2 | import es from './es.json' 3 | 4 | export default { 5 | en, 6 | es 7 | } 8 | -------------------------------------------------------------------------------- /templates/template-universal-js/README.md: -------------------------------------------------------------------------------- 1 | # cra-template-universal 2 | 3 | Template designed to use with [universal-scripts](https://github.com/GlueDigital/universal-scripts). 4 | -------------------------------------------------------------------------------- /templates/template-universal-ts/README.md: -------------------------------------------------------------------------------- 1 | # cra-template-universal 2 | 3 | Template designed to use with [universal-scripts](https://github.com/GlueDigital/universal-scripts). 4 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GlueDigital/universal-scripts/HEAD/templates/template-universal-js/template/src/static/favicon.ico -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GlueDigital/universal-scripts/HEAD/templates/template-universal-ts/template/src/static/favicon.ico -------------------------------------------------------------------------------- /universal-scripts/docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight 2 | 3 | title: Universal Scripts 4 | description: A zero-config build system for complex React apps 5 | show_downloads: 'false' 6 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-ssg/build.conf.d/50-ssg.js: -------------------------------------------------------------------------------- 1 | export const extraDefinitions = async (definitions, opts = {}) => { 2 | return { ...definitions, __SSG__: opts?.ssg ?? false } 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /demo 3 | .DS_Store 4 | .env 5 | 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | universal-scripts*.tgz 10 | universal-plugin*.tgz 11 | 12 | package-lock.json 13 | -------------------------------------------------------------------------------- /universal-scripts/.npmignore: -------------------------------------------------------------------------------- 1 | /docs 2 | /demo 3 | .DS_Store 4 | .env 5 | 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | universal-scripts*.tgz 10 | 11 | package-lock.json 12 | yarn.lock 13 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import user from './slices/user' 2 | 3 | export const reducerList = { 4 | // Add your reducers here 5 | user 6 | } 7 | 8 | export default reducerList 9 | -------------------------------------------------------------------------------- /universal-scripts/lib/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit' 2 | 3 | export const cleanup = createAction('@@universal-scripts/cleanup') 4 | export const clientInit = createAction('@@universal-scripts/client-init') 5 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/routes/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from 'react-intl' 2 | 3 | export default function Home() { 4 | return ( 5 |

6 | 7 |

8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/routes/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from 'react-intl' 2 | 3 | const Home = () => { 4 | return ( 5 |

6 | 7 |

8 | ) 9 | } 10 | 11 | export default Home 12 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router' 2 | import Home from './Home/Home' 3 | 4 | const App = () => ( 5 | 6 | } index /> 7 | 8 | ) 9 | 10 | export default App 11 | -------------------------------------------------------------------------------- /universal-scripts/lib/plugins/trigger.js: -------------------------------------------------------------------------------- 1 | import getConfig from '../../config.js' 2 | 3 | export const triggerHook = (name) => async (initialConfig, opts) => 4 | ((await getConfig(opts.id))[name] ?? []).reduce( 5 | (stylesConfig, enhancer) => enhancer(stylesConfig, opts), 6 | initialConfig 7 | ) 8 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router' 2 | import Home from './Home/Home' 3 | 4 | export default function App() { 5 | return ( 6 | 7 | } index /> 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /universal-scripts/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare const __SSR__: boolean 2 | declare const __DEV__: boolean 3 | declare const __WATCH__: boolean 4 | declare const __SERVER__: boolean 5 | declare const __SSG__: boolean 6 | 7 | declare interface Window { 8 | ___INITIAL_STATE__?: Record 9 | store?: unknown 10 | } 11 | -------------------------------------------------------------------------------- /universal-scripts/lib/redux/lang.ts: -------------------------------------------------------------------------------- 1 | import { updateIntl } from './slices' 2 | import locales from 'src/locales' 3 | 4 | export function setLang(lang: string, setCookie = true) { 5 | if (setCookie) document.cookie = `lang=${lang}` 6 | return updateIntl({ 7 | lang, 8 | messages: locales[lang] 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import user from './slices/user' 2 | 3 | export const reducerList = { 4 | // Add your reducers here 5 | user 6 | } 7 | 8 | export type ReducerType = { 9 | [Key in keyof typeof reducerList]: ReturnType<(typeof reducerList)[Key]> 10 | } 11 | 12 | 13 | export default reducerList 14 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/src/store/slices/user.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | 3 | const initialState = { me: {} } 4 | 5 | const userSlice = createSlice({ 6 | name: 'user', 7 | initialState, 8 | reducers: { 9 | // TODO 10 | } 11 | }) 12 | 13 | export const {} = userSlice.actions 14 | export default userSlice.reducer -------------------------------------------------------------------------------- /templates/template-universal-js/template/src/store/slices/user.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | 3 | const initialState = { me: {} } 4 | 5 | const userSlice = createSlice({ 6 | name: 'user', 7 | initialState, 8 | reducers: { 9 | // TODO 10 | } 11 | }) 12 | 13 | export const {} = userSlice.actions 14 | export default userSlice.reducers -------------------------------------------------------------------------------- /universal-scripts/index.mjs: -------------------------------------------------------------------------------- 1 | import { cleanup, clientInit } from './lib/redux/actions.ts' 2 | import { updateIntl, requestInit } from './lib/redux/slices.ts' 3 | import { useAppDispatch, useAppSelector } from './lib/redux/selector.ts' 4 | import { setLang } from './lib/redux/lang.ts' 5 | 6 | export { 7 | cleanup, 8 | clientInit, 9 | updateIntl, 10 | requestInit, 11 | useAppDispatch, 12 | useAppSelector, 13 | setLang 14 | } 15 | -------------------------------------------------------------------------------- /universal-scripts/lib/redux/selector.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' 2 | import { ServerRootState, ClientRootState, AppDispatch } from './types' 3 | 4 | export const useServerSelector: TypedUseSelectorHook = 5 | useSelector 6 | 7 | export const useAppSelector: TypedUseSelectorHook = useSelector 8 | 9 | export const useAppDispatch = useDispatch.withTypes() 10 | -------------------------------------------------------------------------------- /universal-scripts/index.cjs: -------------------------------------------------------------------------------- 1 | const { cleanup, clientInit } = require('./lib/redux/actions') 2 | const { updateIntl, requestInit } = require('./lib/redux/slices') 3 | const { setLang } = require('./lib/redux/lang') 4 | 5 | const { useAppDispatch, useAppSelector } = require('./lib/redux/selector') 6 | 7 | module.exports = { 8 | cleanup, 9 | clientInit, 10 | updateIntl, 11 | requestInit, 12 | useAppDispatch, 13 | useAppSelector, 14 | setLang 15 | } 16 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/client/70-router.tsx: -------------------------------------------------------------------------------- 1 | import { ClientRoot } from '../../lib/redux/types' 2 | import { BrowserRouter } from 'react-router' 3 | // @ts-expect-error Imported from the project 4 | import App from 'src/routes' 5 | 6 | const routerRoot: ClientRoot = () => { 7 | // On the client, just use BrowserRouter 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export const reactRoot = routerRoot 16 | -------------------------------------------------------------------------------- /universal-scripts/scripts/start.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | // Babel will complain if no NODE_ENV. Set it if needed. 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 5 | 6 | const builder = (await import('../lib/builder.js')).default 7 | const compiler = await builder({ isWatch: true }) 8 | 9 | global.__WATCH__ = true // Signal to server that we're doing HMR 10 | 11 | const server = (await import('../server/main.js')).default 12 | server(compiler) 13 | -------------------------------------------------------------------------------- /universal-scripts/build.conf.d/11-externals.js: -------------------------------------------------------------------------------- 1 | import nodeExternals from 'webpack-node-externals' 2 | 3 | const enhancer = (_, config) => { 4 | if (config.target === 'node') { 5 | // Don't bundle node_modules for the server: node can access it directly 6 | config.externals = [ 7 | nodeExternals({ 8 | allowlist: ['universal-scripts', 'js.conf.d-webpack/src'] 9 | }) 10 | ] 11 | } 12 | return config 13 | } 14 | 15 | export const webpack = enhancer 16 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-helmet/runtime.conf.d/client/19-helmet.tsx: -------------------------------------------------------------------------------- 1 | import { Helmet, HelmetProvider } from 'react-helmet-async' 2 | import { defaultHeaders } from '../../lib/headers' 3 | 4 | const addDefaultHeaders = async (ctx, next) => { 5 | return ( 6 | 7 | {defaultHeaders(ctx.store)} 8 | {await next()} 9 | 10 | ) 11 | } 12 | 13 | export const reactRoot = addDefaultHeaders 14 | -------------------------------------------------------------------------------- /universal-scripts/scripts/plugin.js: -------------------------------------------------------------------------------- 1 | import { 2 | findUniversalPlugins, 3 | findScriptInPlugin 4 | } from '../lib/find-scripts.js' 5 | import spawn from 'cross-spawn' 6 | 7 | const args = process.argv.slice(3) 8 | 9 | const pluginScriptPath = findScriptInPlugin( 10 | findUniversalPlugins(), 11 | process.argv[2] 12 | ) 13 | 14 | console.log(pluginScriptPath) 15 | 16 | const result = spawn.sync('node', [pluginScriptPath, ...args], { 17 | stdio: 'inherit' 18 | }) 19 | 20 | process.exit(result.status) 21 | -------------------------------------------------------------------------------- /templates/template-universal-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-template-universal", 3 | "version": "2.2.1", 4 | "keywords": [ 5 | "react", 6 | "create-react-app", 7 | "template", 8 | "universal" 9 | ], 10 | "description": "A Create React App template to use with universal-scripts", 11 | "repository": "GlueDigital/cra-template-universal", 12 | "author": "Glue Digital ", 13 | "license": "MIT", 14 | "files": [ 15 | "template", 16 | "template.json" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /universal-scripts/lib/render-html-layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates HTML for a given Helmet head, body, and extra head components. 3 | */ 4 | export default (head, styles) => 5 | '' + 6 | '' + 9 | '' + 10 | head.title.toString() + 11 | head.meta.toString() + 12 | head.base.toString() + 13 | head.link.toString() + 14 | head.script.toString() + 15 | head.style.toString() + 16 | (styles ? styles.join('') : '') + 17 | '' + 18 | '' + 19 | '
' 20 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-helmet/lib/headers.tsx: -------------------------------------------------------------------------------- 1 | // These are the default headers that can be overriden on app code. 2 | export const defaultHeaders = (store: { 3 | getState: () => { intl: { lang: string } } 4 | }) => { 5 | const state = store && store.getState() 6 | const lang = state && state.intl.lang 7 | return ( 8 | <> 9 | 10 | 11 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /templates/template-universal-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-template-universal-ts", 3 | "version": "2.2.2", 4 | "keywords": [ 5 | "react", 6 | "template", 7 | "universal", 8 | "typescript" 9 | ], 10 | "description": "A Typescript template to use with universal-scripts", 11 | "repository": "GlueDigital/cra-template-universal-ts", 12 | "author": "Glue Digital ", 13 | "license": "MIT", 14 | "files": [ 15 | "template", 16 | "template.json" 17 | ], 18 | "scripts": { 19 | "test": "scripts/run-tests.sh" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-jest/lib/swcTransform.js: -------------------------------------------------------------------------------- 1 | import swcJest from '@swc/jest' 2 | 3 | export default swcJest.createTransformer({ 4 | jsc: { 5 | parser: { 6 | syntax: 'typescript', 7 | jsx: true, 8 | tsx: true 9 | }, 10 | transform: { 11 | react: { 12 | runtime: 'automatic' // Equivalent to '@babel/preset-react' 13 | } 14 | }, 15 | target: 'es2022', // Similar to @babel/preset-env 16 | externalHelpers: true // Equivalent to '@babel/plugin-transform-runtime' 17 | }, 18 | swcrc: false, 19 | configFile: false 20 | }) 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-scripts-monorepo", 3 | "private": true, 4 | "workspaces": [ 5 | "universal-scripts", 6 | "universal-plugins/*", 7 | "templates/*" 8 | ], 9 | "devDependencies": { 10 | "globals": "^15.14.0", 11 | "eslint": "^9.19.0", 12 | "eslint-config-prettier": "^10.0.1", 13 | "eslint-plugin-import": "^2.30.0", 14 | "eslint-plugin-n": "^17.15.1", 15 | "eslint-plugin-prettier": "^5.2.3", 16 | "eslint-plugin-react": "^7.37.4", 17 | "eslint-plugin-react-hooks": "^5.1.0", 18 | "typescript-eslint": "^8.23.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-ssg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gluedigital/universal-plugin-ssg", 3 | "version": "1.0.2", 4 | "private": false, 5 | "type": "module", 6 | "files": [ 7 | "build.conf.d", 8 | "runtime.conf.d", 9 | "scripts" 10 | ], 11 | "scripts": { 12 | "lint": "eslint .", 13 | "lint:fix": "eslint . --fix", 14 | "prettier": "prettier --write ." 15 | }, 16 | "keywords": [ 17 | "universal-scripts", 18 | "universal-plugin", 19 | "Static Site Generation", 20 | "SSG" 21 | ], 22 | "author": "Alex Sanchez", 23 | "license": "ISC", 24 | "description": "Enable Static Site Generation in universal-scripts" 25 | } 26 | -------------------------------------------------------------------------------- /universal-scripts/server/serverMiddleware.js: -------------------------------------------------------------------------------- 1 | import config from '#js.conf.d' 2 | 3 | // Simple system to trigger middleware-like hooks 4 | const triggerHook = (name) => (req, res, initial) => 5 | config[name] && 6 | config[name].reduceRight( 7 | (thisNext, hook) => () => hook(req, res, thisNext), 8 | () => initial 9 | )() 10 | 11 | const triggeringMiddleware = (req, res, next) => { 12 | req.triggerHook = triggerHook 13 | return next() 14 | } 15 | 16 | export const startup = () => 17 | config.startup && Promise.all(config.startup.map((x) => x())) 18 | 19 | export const rawConfig = config 20 | 21 | export default [triggeringMiddleware, ...config.serverMiddleware] 22 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-helmet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gluedigital/universal-plugin-helmet", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "lint": "eslint .", 7 | "lint:fix": "eslint . --fix", 8 | "prettier": "prettier --write ." 9 | }, 10 | "keywords": [ 11 | "react-helmet", 12 | "universal-scripts", 13 | "universal-plugin" 14 | ], 15 | "engines": { 16 | "node": ">=18" 17 | }, 18 | "author": "Alex Sanchez", 19 | "license": "ISC", 20 | "description": "Enables full functionality of react-helmet-async for Universal-Scripts", 21 | "dependencies": { 22 | "react-helmet-async": "^2.0.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /universal-scripts/bin/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | rm -rf universal-scripts-*.tgz ../test 5 | 6 | echo "Running linter..." 7 | yarn lint 8 | echo "📦 Building local package..." 9 | npm pack 10 | 11 | cd .. 12 | mkdir test 13 | cd test 14 | 15 | yarn create universal-scripts test-app 16 | 17 | echo "✅ Generated Project: ../test/test-app" 18 | 19 | cd ../test/test-app 20 | 21 | yarn add ../../universal-scripts/universal-scripts-*.tgz 22 | 23 | echo "✅ Installed universal-scripts" 24 | 25 | yarn build 26 | echo "✅ Successful build" 27 | 28 | # 🧹 Cleanup 29 | echo "🧹 Cleaning..." 30 | cd ../.. 31 | rm -rf test universal-scripts-*.tgz 32 | 33 | echo "🏁 All Ok." 34 | -------------------------------------------------------------------------------- /universal-scripts/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AppDispatch, 3 | ClientRootState, 4 | ServerRootState, 5 | ClientStore, 6 | ServerStore 7 | } from './lib/redux/types' 8 | 9 | import { setLang } from './lib/redux/lang' 10 | 11 | import { cleanup, clientInit } from './lib/redux/actions' 12 | import { requestInit, updateIntl } from './lib/redux/slices' 13 | 14 | import { useAppDispatch, useAppSelector } from './lib/redux/selector' 15 | 16 | export { 17 | AppDispatch, 18 | ClientRootState, 19 | ServerRootState, 20 | ServerStore, 21 | ClientStore, 22 | cleanup, 23 | clientInit, 24 | requestInit, 25 | updateIntl, 26 | useAppDispatch, 27 | useAppSelector, 28 | setLang 29 | } 30 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/client/50-render.tsx: -------------------------------------------------------------------------------- 1 | import { ClientInit } from '../../lib/redux/types' 2 | import ReactDOM from 'react-dom/client' 3 | 4 | let rendered 5 | 6 | const clientRender: ClientInit = async (ctx) => { 7 | const rootNode = document.getElementById('root') 8 | const children = await ctx.triggerHook('reactRoot')(ctx, false) 9 | if (rendered) { 10 | rendered.render(children) 11 | return 12 | } 13 | if (__SSR__) { 14 | rendered = ReactDOM.hydrateRoot(rootNode, children) 15 | } else { 16 | const root = ReactDOM.createRoot(rootNode) 17 | root.render(children) 18 | rendered = root 19 | } 20 | } 21 | 22 | export const clientInit = clientRender 23 | -------------------------------------------------------------------------------- /universal-scripts/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import 'dotenv/config' 4 | 5 | process.env.NODE_ENV = 'test' 6 | 7 | process.on('unhandledRejection', (err) => { 8 | throw err 9 | }) 10 | 11 | import getConfig from '../config.js' 12 | import chalk from 'chalk' 13 | 14 | const configs = await getConfig('test') 15 | const testRunner = configs.test.reduce((cfg, enhancer) => enhancer({}, cfg), {}) 16 | 17 | if (typeof testRunner !== 'function') { 18 | console.warn( 19 | chalk.yellow( 20 | `${chalk.bold('⚠️ Error')}: No test runner configuration found. Please create a config or install ${chalk.underline('universal-plugin-jest')}.` 21 | ) 22 | ) 23 | } else { 24 | testRunner() 25 | } 26 | -------------------------------------------------------------------------------- /universal-scripts/bin/universal-scripts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This script just executes the appropiate script on the scripts folder. 4 | import spawn from 'cross-spawn' 5 | import { fileURLToPath } from 'url' 6 | import { resolve, dirname } from 'path' 7 | 8 | const script = process.argv[2] 9 | const args = process.argv.slice(3) 10 | 11 | const __filename = fileURLToPath(import.meta.url) 12 | const __dirname = dirname(__filename) 13 | 14 | const scriptPath = resolve(__dirname, '../scripts/', script) 15 | 16 | // Can be executed with this line 17 | // await import(`../scripts/${script}.js`) 18 | 19 | const result = spawn.sync('node', [scriptPath, ...args], { stdio: 'inherit' }) 20 | 21 | process.exit(result.status) 22 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gluedigital/universal-plugin-jest", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "files": [ 6 | "build.conf.d", 7 | "lib" 8 | ], 9 | "scripts": { 10 | "lint": "eslint .", 11 | "lint:fix": "eslint . --fix", 12 | "prettier": "prettier --write ." 13 | }, 14 | "keywords": [ 15 | "Jest", 16 | "test", 17 | "universal-scripts", 18 | "universal-plugin" 19 | ], 20 | "engines": { 21 | "node": ">=18" 22 | }, 23 | "author": "Alex Sanchez", 24 | "license": "ISC", 25 | "description": "Jest configuration to enable Jest on universal-scripts", 26 | "dependencies": { 27 | "@swc/jest": "^0.2.37", 28 | "jest": "^29.7.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "include": [ 4 | "universal-scripts/runtime.conf.d/**/*", 5 | "universal-scripts/globals.d.ts", 6 | "universal-scripts/@types", 7 | "universal-scripts/lib/redux/**/*", 8 | "universal-plugins" 9 | ], 10 | "compilerOptions": { 11 | "allowSyntheticDefaultImports": true, 12 | "lib": ["dom", "DOM.Iterable", "ES2022"], 13 | "jsx": "react-jsx", 14 | "target": "ES2022", 15 | "module": "ESNext", 16 | "moduleResolution": "node", 17 | "baseUrl": ".", 18 | "sourceMap": true, 19 | "skipLibCheck": true, 20 | "typeRoots": [ 21 | "universal-scripts/node_modules/@types", 22 | "universal-scripts/@types" 23 | ], 24 | "esModuleInterop": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-sass/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gluedigital/universal-plugin-sass", 3 | "version": "1.0.2", 4 | "type": "module", 5 | "files": [ 6 | "build.conf.d" 7 | ], 8 | "scripts": { 9 | "lint": "eslint .", 10 | "lint:fix": "eslint . --fix", 11 | "prettier": "prettier --write ." 12 | }, 13 | "keywords": [ 14 | "universal-scripts", 15 | "universal-plugin", 16 | "styles", 17 | "sass" 18 | ], 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "author": "Alex Sanchez", 23 | "license": "ISC", 24 | "description": "Sass configuration for universal-scripts", 25 | "dependencies": { 26 | "css-loader": "^7.1.2", 27 | "sass": "^1.86.3", 28 | "sass-loader": "^16.0.5" 29 | }, 30 | "peerDependencies": { 31 | "universal-scripts": ">=3.5.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-helmet/runtime.conf.d/server/19-helmet.tsx: -------------------------------------------------------------------------------- 1 | import { Helmet, HelmetProvider } from 'react-helmet-async' 2 | import { Request, Response } from 'express' 3 | import { ReactNode } from 'react' 4 | import { defaultHeaders } from '../../lib/headers' 5 | 6 | export const extraHead = (req, res) => { 7 | const head = req.helmetContext.helmet 8 | return head 9 | } 10 | 11 | const addHelmet = async ( 12 | req: Request, 13 | res: Response, 14 | next: () => Promise 15 | ) => { 16 | if (!req.helmetContext) { 17 | req.helmetContext = { 18 | helmet: null 19 | } 20 | } 21 | return ( 22 | 23 | {defaultHeaders(req.store)} 24 | {await next()} 25 | 26 | ) 27 | } 28 | 29 | export const reactRoot = addHelmet 30 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/server/70-router.tsx: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | 3 | let StaticRouter 4 | let isV6 = true 5 | 6 | try { 7 | // Import v6 8 | StaticRouter = require('react-router/server').StaticRouter 9 | } catch { 10 | isV6 = false 11 | // Try to import v5 12 | StaticRouter = require('react-router').StaticRouter 13 | } 14 | 15 | const App = __SSR__ && require('src/routes').default 16 | 17 | const routerRoot = (req: Request) => { 18 | if (!App) return null 19 | 20 | const url = req.url 21 | 22 | if (isV6) { 23 | return ( 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | req.renderCtx = {} 31 | 32 | return ( 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | export const reactRoot = routerRoot 40 | -------------------------------------------------------------------------------- /universal-scripts/client/init.js: -------------------------------------------------------------------------------- 1 | import config from '#js.conf.d' 2 | 3 | // Simple system to trigger middleware-like hooks 4 | const triggerHook = (name) => (ctx, initial) => 5 | config[name] && 6 | config[name].reduceRight( 7 | (thisNext, hook) => () => hook(ctx, thisNext), 8 | () => initial 9 | )() 10 | 11 | const initialize = () => { 12 | const ctx = { triggerHook } 13 | triggerHook('clientInit')(ctx, false) 14 | } 15 | 16 | initialize() 17 | // https://github.com/webpack-contrib/webpack-hot-middleware/issues/23 18 | // Enable HMR 19 | if (import.meta.webpackHot) { 20 | import.meta.webpackHot.accept() 21 | const webpackHotMiddlewareClient = await import( 22 | 'webpack-hot-middleware/client.js' 23 | ) 24 | webpackHotMiddlewareClient.subscribe(function (message) { 25 | if (message.action === 'reload-page') { 26 | window.location.reload() 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/server/07-static.tsx: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import express from 'express' 4 | 5 | const appDirectory = fs.realpathSync(process.cwd()) 6 | 7 | // TODO: pay attention to basename 8 | // const basename = process.env.SUBDIRECTORY || '/' 9 | 10 | let serveStatic 11 | if (__WATCH__) { 12 | // Serve static files directly from src (no need to copy again and again) 13 | serveStatic = express.static(path.resolve(appDirectory, 'src', 'static'), { 14 | dotfiles: 'allow' 15 | }) 16 | } else { 17 | // Serve files from the build folder (includes copied assets) 18 | serveStatic = express.static(path.resolve(appDirectory, 'build', 'client'), { 19 | dotfiles: 'allow', 20 | setHeaders: (res) => { 21 | res.setHeader('Cache-Control', 'public,max-age=31536000,immutable') 22 | } 23 | }) 24 | } 25 | 26 | export const serverMiddleware = serveStatic 27 | -------------------------------------------------------------------------------- /universal-scripts/scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import 'dotenv/config' 3 | 4 | // Babel will complain if no NODE_ENV. Set it if needed. 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 6 | 7 | import chalk from 'chalk' 8 | 9 | import fs from 'fs-extra' 10 | fs.remove('./build/') 11 | 12 | const builder = (await import('../lib/builder.js')).default 13 | 14 | const compiler = await builder() 15 | compiler.run((err, stats) => { 16 | let hasErrors = err 17 | if (!stats) { 18 | console.log(err) 19 | console.log(chalk.red.bold('\nBuild failed.\n')) 20 | process.exit(1) 21 | } 22 | for (const build of stats.stats) { 23 | if (build.compilation.errors && build.compilation.errors.length) { 24 | hasErrors = true 25 | break 26 | } 27 | } 28 | if (hasErrors) { 29 | console.log(chalk.red.bold('\nBuild failed.\n')) 30 | process.exit(1) 31 | } 32 | process.exit(0) 33 | }) 34 | -------------------------------------------------------------------------------- /universal-scripts/docs/migration.md: -------------------------------------------------------------------------------- 1 | ## Migrating from v1 2 | 3 | Universal Scripts v2 is a huge change from v1, and we couldn't apply all the updates while keeping backwards compatibility. 4 | If you have a project on v1, we recommend that you can keep using it, but if you're set on upgrading to v2, check the changelog below to find out which changes you'll need to make for it to keep working. 5 | 6 | ### Changelog 7 | 8 | - Removed fetchData support. Fix: replace it with useFetch 9 | - Updated react-router to v5. Fix: update your routes at `src/routes/` with the new syntax 10 | - Updated react-intl to v3. Fix: remove anything related to `locale-data` from `src/locales/index.js` 11 | - Replaced react-helmet with react-helmet-async. Fix: update your imports 12 | - Removed `src/index.js`. Fix: move your code to `runtime.conf.d/client/05-init.js` 13 | - Removed `src/routes/serverRoutes.js`. Fix: move your code to `runtime.conf.d/server/05-api.js` 14 | - Renamed `src/styles/main.sass` to `src/styles/index.sass`. 15 | -------------------------------------------------------------------------------- /universal-scripts/@types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | import * as express from 'express' 3 | import { StatsCompilation } from 'webpack/types' 4 | import { HelmetServerState } from 'react-helmet-async' 5 | import { StaticRouterContext } from 'react-router' 6 | import { PipeableStream } from 'react-dom/server' 7 | import { ReactNode } from 'react' 8 | import { Store } from 'lib/redux/types' 9 | 10 | declare global { 11 | namespace Express { 12 | interface Request { 13 | clientStats: StatsCompilation 14 | assets: { 15 | scripts: string[] 16 | styles: string[] 17 | } 18 | helmetContext: { 19 | helmet: HelmetServerState 20 | } 21 | store: Store 22 | renderCtx: StaticRouterContext 23 | stream: PipeableStream 24 | triggerHook: ( 25 | name: string 26 | ) => (req: Request, res: Response, initial: boolean) => ReactNode 27 | initExtras: Record 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /templates/template-universal-js/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "dependencies": { 4 | "react": "^19.1.0", 5 | "react-dom": "^19.1.0", 6 | "react-intl": "^7.1.10", 7 | "react-redux": "^9.2.0", 8 | "react-router": "^7.5.0", 9 | "@reduxjs/toolkit": "^2.6.1" 10 | }, 11 | "devDependencies": { 12 | "eslint": "^9.24.0", 13 | "globals": "^16.0.0", 14 | "@eslint/js": "^9.24.0", 15 | "eslint-config-prettier": "^10.1.1", 16 | "eslint-plugin-prettier": "^5.2.6", 17 | "eslint-plugin-react": "^7.37.5", 18 | "eslint-plugin-import": "^2.31.0", 19 | "eslint-plugin-react-hooks": "^5.2.0", 20 | "eslint-plugin-n": "^17.17.0", 21 | "prettier": "^3.5.3", 22 | "@types/react": "^19.1.0", 23 | "@types/react-dom": "^19.1.2", 24 | "@types/express": "^5.0.1", 25 | "@types/jest": "^29.5.14" 26 | }, 27 | "scripts": { 28 | "lint:fix": "eslint src --fix", 29 | "prettier": "prettier -- --write" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const value: string 3 | export default value 4 | } 5 | 6 | declare module '*.jpg' { 7 | const value: string 8 | export default value 9 | } 10 | 11 | declare module '*.svg' { 12 | const value: string 13 | export default value 14 | } 15 | 16 | declare module '*.webp' { 17 | const value: string 18 | export default value 19 | } 20 | 21 | declare module '*.mp4' { 22 | const value: string 23 | export default value 24 | } 25 | 26 | declare module '*.webm' { 27 | const value: string 28 | export default value 29 | } 30 | 31 | declare module '*.css' { 32 | const content: { [className: string]: string } 33 | export default content 34 | } 35 | 36 | declare module '*.sass' { 37 | const content: { [className: string]: string } 38 | export default content 39 | } 40 | 41 | declare const __SERVER__: boolean 42 | declare const __CLIENT__: boolean 43 | declare const __DEV__: boolean 44 | declare const __PROD__: boolean 45 | declare const __WATCH__: boolean -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-sass/build.conf.d/32-sass.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { getPluginsConfig } from 'universal-scripts/lib/universal-config.js' 3 | 4 | /** 5 | * @param {{ loaders: loader[], exts: string[] }} styleConfig [styleLoader, cssLoader, postCssLoader]... 6 | * @param {*} opts General opts for build 7 | * @returns updated loaders and extensions 8 | */ 9 | export async function stylesExtras(styleConfig, opts) { 10 | const [css] = styleConfig 11 | const { loaders } = css 12 | 13 | const sassConfig = await getPluginsConfig('sass') 14 | 15 | const sassLoader = { 16 | loader: 'sass-loader', 17 | options: { 18 | sourceMap: true, 19 | sassOptions: { 20 | includePaths: [resolve('appDirectory', 'src', 'styles')], 21 | quietDeps: true, 22 | exclude: ['import'], 23 | ...sassConfig 24 | } 25 | } 26 | } 27 | 28 | return [ 29 | ...styleConfig, 30 | { 31 | loaders: [...loaders, sassLoader].filter(Boolean), 32 | exts: ['scss', 'sass'] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "include": ["src/**/*", "globals.d.ts"], 4 | "compilerOptions": { 5 | "allowSyntheticDefaultImports": true, 6 | "lib": [ 7 | "dom", 8 | "DOM.Iterable", 9 | "ES2022" 10 | ], 11 | "jsx": "react-jsx", 12 | "target": "ES2022", 13 | "module": "ESNext", 14 | "moduleResolution": "node", 15 | "baseUrl": ".", 16 | "noImplicitAny": false, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "esModuleInterop": true, 20 | "resolveJsonModule": true, 21 | "removeComments": false, 22 | "preserveConstEnums": true, 23 | "sourceMap": true, 24 | "skipLibCheck": true, 25 | "paths": { 26 | "@components/*": ["./src/components/*"], 27 | "@utils/*": ["./src/utils/*"], 28 | "@routes/*": ["./src/routes/*"], 29 | "@static/*": ["./src/static/*"], 30 | "@hooks/*": ["./src/hooks/*"], 31 | "src/*": ["./src/*"], 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "dependencies": { 4 | "react": "^19.1.0", 5 | "react-dom": "^19.1.0", 6 | "react-intl": "^7.1.10", 7 | "react-redux": "^9.2.0", 8 | "react-router": "^7.5.0", 9 | "@reduxjs/toolkit": "^2.6.1" 10 | }, 11 | "devDependencies": { 12 | "eslint": "^9.24.0", 13 | "globals": "^16.0.0", 14 | "@eslint/js": "^9.24.0", 15 | "eslint-config-prettier": "^10.1.1", 16 | "eslint-plugin-prettier": "^5.2.6", 17 | "eslint-plugin-react": "^7.37.5", 18 | "eslint-plugin-import": "^2.31.0", 19 | "eslint-plugin-react-hooks": "^5.2.0", 20 | "eslint-plugin-n": "^17.17.0", 21 | "prettier": "^3.5.3", 22 | "typescript": "^5.8.3", 23 | "@types/react": "^19.1.0", 24 | "@types/react-dom": "^19.1.2", 25 | "@types/express": "^5.0.1", 26 | "@types/jest": "^29.5.14", 27 | "typescript-eslint": "8.29.1" 28 | }, 29 | "scripts": { 30 | "lint:fix": "eslint src --fix", 31 | "prettier": "prettier -- --write" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) GlueDigital and its affiliates. 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 | -------------------------------------------------------------------------------- /templates/template-universal-js/template/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import eslintPluginPrettier from 'eslint-plugin-prettier/recommended' 5 | import { FlatCompat } from '@eslint/eslintrc' 6 | 7 | const compat = new FlatCompat() 8 | 9 | // TODO Add config 10 | // import nodePlugin from 'eslint-plugin-n' 11 | 12 | // Waiting to support eslint 9 13 | // import importPlugin from 'eslint-plugin-import' 14 | 15 | export default [ 16 | eslint.configs.recommended, 17 | // nodePlugin.configs['flat/recommended-script'], 18 | react.configs.flat.recommended, 19 | react.configs.flat['jsx-runtime'], 20 | eslintPluginPrettier, 21 | ...compat.config({ 22 | extends: ['plugin:react-hooks/recommended'] 23 | }), 24 | { 25 | files: ['**/*.{js,jsx,mjs,cjs}'], 26 | languageOptions: { 27 | globals: { 28 | ...globals.browser, 29 | ...globals.node, 30 | ...globals.es2022, 31 | __DEV__: 'readonly', 32 | __PROD__: 'readonly', 33 | __SERVER__: 'readonly', 34 | __CLIENT__: 'readonly', 35 | __WATCH__: 'readonly' 36 | } 37 | } 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /universal-scripts/lib/redux/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { createClientStore, createServerStore } from './store' 3 | import { HelmetServerState } from 'react-helmet-async' 4 | 5 | // @ts-expect-error Imported from the project 6 | import { ReducerType } from 'src/store/reducers' 7 | 8 | export type ServerRootState = ReturnType< 9 | ReturnType['getState'] 10 | > & 11 | ReducerType 12 | export type ClientRootState = ReturnType< 13 | ReturnType['getState'] 14 | > & 15 | ReducerType 16 | export type AppDispatch = ReturnType['dispatch'] 17 | export type ServerStore = ReturnType 18 | export type ClientStore = ReturnType 19 | 20 | interface Context { 21 | helmetContext: { 22 | helmet: HelmetServerState 23 | } 24 | store: ClientStore 25 | triggerHook: (name: string) => (ctx: Context, initial: boolean) => ReactNode 26 | } 27 | 28 | export type ClientInit = ( 29 | ctx: Context, 30 | next: () => ReactNode 31 | ) => ReactNode | Promise 32 | export type ClientRoot = ( 33 | ctx: Context, 34 | next: () => Promise 35 | ) => Promise | ReactNode 36 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js' 4 | import globals from 'globals' 5 | import tseslint from 'typescript-eslint' 6 | import react from 'eslint-plugin-react' 7 | import eslintPluginPrettier from 'eslint-plugin-prettier/recommended' 8 | import { FlatCompat } from '@eslint/eslintrc' 9 | 10 | const compat = new FlatCompat() 11 | 12 | const universalGlobals = { 13 | __BUILD__: false, 14 | __DEV__: false, 15 | __PROD__: false, 16 | __SERVER__: false, 17 | __CLIENT__: false, 18 | __WATCH__: false, 19 | __SSR__: false 20 | } 21 | 22 | export default [ 23 | eslint.configs.recommended, 24 | ...tseslint.configs.recommended, 25 | react.configs.flat?.recommended, 26 | react.configs.flat?.['jsx-runtime'], 27 | eslintPluginPrettier, 28 | ...compat.config({ 29 | extends: ['plugin:react-hooks/recommended'] 30 | }), 31 | { 32 | files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], 33 | languageOptions: { 34 | globals: { 35 | ...globals.browser, 36 | ...globals.es2022, 37 | ...globals.node, 38 | ...universalGlobals 39 | } 40 | }, 41 | rules: { 42 | '@typescript-eslint/no-unused-vars': 'warn', 43 | '@typescript-eslint/no-require-imports': 'off' 44 | } 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /universal-scripts/config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import jsconfd from 'js.conf.d' 4 | import { fileURLToPath } from 'url' 5 | import { 6 | findUniversalPlugins, 7 | filterPluginsWithSubdir 8 | } from './lib/find-scripts.js' 9 | 10 | const __filename = fileURLToPath(import.meta.url) 11 | const __dirname = path.dirname(__filename) 12 | 13 | const arrayMerger = (current, add) => { 14 | for (const key of Object.keys(add)) { 15 | current[key] = current[key] || [] 16 | current[key].push(add[key]) 17 | } 18 | return current 19 | } 20 | 21 | // Load the config directories, using array merger 22 | const appDirectory = fs.realpathSync(process.cwd()) 23 | const libConfig = path.resolve(__dirname, 'build.conf.d') 24 | const userConfig = path.resolve(appDirectory, 'build.conf.d') 25 | const pluginsConfig = filterPluginsWithSubdir( 26 | findUniversalPlugins(), 27 | 'build.conf.d' 28 | ).map((plugin) => path.join(plugin, 'build.conf.d')) 29 | 30 | export default async function getConfig(target) { 31 | const specificUserConfig = path.resolve(userConfig, target) 32 | const config = await jsconfd( 33 | [libConfig, userConfig, specificUserConfig, ...pluginsConfig], 34 | { 35 | merge: arrayMerger 36 | } 37 | ) 38 | return config 39 | } 40 | -------------------------------------------------------------------------------- /universal-scripts/build.conf.d/12-globals.js: -------------------------------------------------------------------------------- 1 | import webpackPackage from 'webpack' 2 | import { triggerHook } from '../lib/plugins/trigger.js' 3 | import { getUniversalConfig } from '../lib/universal-config.js' 4 | 5 | const { DefinePlugin } = webpackPackage 6 | 7 | const enhancer = async (opts = {}, config) => { 8 | const isWatch = opts.isWatch 9 | const isProd = process.env.NODE_ENV === 'production' 10 | const noSsr = await getUniversalConfig('noSsr') 11 | 12 | const ssr = noSsr == null ? true : !noSsr 13 | 14 | const definitions = { 15 | __BUILD__: opts.id, 16 | __PROD__: isProd, 17 | __DEV__: !isProd, 18 | __SERVER__: opts.id === 'server', 19 | __CLIENT__: opts.id === 'client', 20 | __WATCH__: isWatch, 21 | __SSR__: ssr 22 | } 23 | 24 | const allDefinitions = await triggerHook('extraDefinitions')( 25 | definitions, 26 | opts 27 | ) 28 | 29 | if (!config.plugins) config.plugins = [] 30 | if (opts.id === 'client') { 31 | config.plugins.push( 32 | new DefinePlugin({ 33 | ...allDefinitions, 34 | 'process.env': 'window.__ENV_VARS__' 35 | }) 36 | ) 37 | } else { 38 | config.plugins.push(new DefinePlugin(allDefinitions)) 39 | } 40 | 41 | return config 42 | } 43 | 44 | export const webpack = enhancer 45 | -------------------------------------------------------------------------------- /universal-scripts/lib/vars/EnvPlugin.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import dotenv from 'dotenv' 4 | 5 | export class EnvReloadPlugin { 6 | constructor() { 7 | this.envPath = path.resolve(process.cwd(), '.env') 8 | this.startTime = Date.now() 9 | } 10 | 11 | apply(compiler) { 12 | let recompiling = false 13 | 14 | compiler.hooks.afterPlugins.tap('ReloadDotenvPlugin', () => { 15 | fs.watchFile(this.envPath, { interval: 500 }, (curr, prev) => { 16 | const fileModifiedTime = curr.mtimeMs 17 | 18 | if ( 19 | fileModifiedTime <= this.startTime || 20 | curr.mtimeMs === prev.mtimeMs || 21 | recompiling 22 | ) { 23 | return 24 | } 25 | 26 | recompiling = true 27 | 28 | console.log('🔄 File .env changed, reloading vars...') 29 | 30 | dotenv.config({ path: this.envPath, override: true }) 31 | 32 | if (compiler.watching) { 33 | compiler.watching.invalidate(() => { 34 | console.log('♻ Webpack recompiling with new .env vars') 35 | 36 | if (compiler.webpackHotMiddleware) { 37 | compiler.webpackHotMiddleware.publish({ action: 'reload-page' }) 38 | } 39 | 40 | setTimeout(() => (recompiling = false), 1000) 41 | }) 42 | } 43 | }) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/client/30-redux.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux' 2 | import { createClientStore } from '../../lib/redux/store' 3 | import { clientInit as clientInitAction } from '../../lib/redux/actions' 4 | import { ClientInit, ClientRoot } from '../../lib/redux/types' 5 | 6 | const clientRedux: ClientInit = (ctx, next) => { 7 | // Create store using server data (if available) 8 | const initialState = window.___INITIAL_STATE__ || {} 9 | const store = createClientStore(initialState) 10 | 11 | // Make it available through the context 12 | ctx.store = store 13 | 14 | // We now expose the store through window 15 | // This is useful for debugging, and for some special cases where 16 | // the store is needed from outside react (ie: index.js, ...). 17 | // It should NOT be a substitute to connect or the other Redux utils. 18 | // Use with caution. 19 | window.store = store 20 | 21 | // Dispatch a CLIENT_INIT action to give middlewares, etc. a chance of 22 | // updating the store before render 23 | store.dispatch(clientInitAction()) 24 | 25 | // Run any other middlewares 26 | return next() 27 | } 28 | 29 | export const clientInit = clientRedux 30 | 31 | const renderIntlProvider: ClientRoot = async (ctx, next) => ( 32 | {await next()} 33 | ) 34 | 35 | export const reactRoot = renderIntlProvider 36 | -------------------------------------------------------------------------------- /universal-scripts/docs/data-fetching.md: -------------------------------------------------------------------------------- 1 | ## Data fetching 2 | 3 | Loading external data is one of the most basic needs that has to be adapted for universal apps. Universal Scripts is integrated with [ruse-fetch](https://www.npmjs.com/package/ruse-fetch), which provides a modern way of fetching data, making use of fetch and Suspense. 4 | 5 | Everything is already configured, so you can use it just like this: 6 | 7 | ```javascript 8 | import React from 'react' 9 | import { useFetch } from 'ruse-fetch' 10 | 11 | const Home = () => { 12 | const res = useFetch('https://reqres.in/api/users') 13 | return ( 14 |
    15 | {res.data.map((u) => ( 16 |
  • u.first_name
  • 17 | ))} 18 |
19 | ) 20 | } 21 | ``` 22 | 23 | To handle the loading state, use ``, and to handle API errors, you can use a error boundary. 24 | 25 | When rendering the page on the server, it will wait for all useFetch hooks, rendering the page with them already resolved. The responses will be sent to the client, so it doesn't have to repeat them on initial render. Any subsequent calls (eg. after clicking a Link) will be performed on the client. 26 | 27 | You can learn more about the library on their [page](https://www.npmjs.com/package/ruse-fetch). 28 | 29 | ## Next steps 30 | 31 | Now that you can easily do API calls, you might want to learn more about the [store](store). 32 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-ssg/scripts/ssg.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import 'dotenv/config' 3 | import spawn from 'cross-spawn' 4 | import { realpathSync } from 'fs' 5 | 6 | // Babel will complain if no NODE_ENV. Set it if needed. 7 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 8 | 9 | import chalk from 'chalk' 10 | 11 | import fs from 'fs-extra' 12 | 13 | fs.remove('./build/') 14 | 15 | global.__WATCH__ = false // Signal to server that we are not doing HMR 16 | global.__SSG__ = true 17 | 18 | const builder = (await import('universal-scripts/lib/builder.js')).default 19 | 20 | const compiler = await builder({ 21 | ssg: true 22 | }) 23 | 24 | compiler.run((err, stats) => { 25 | let hasErrors = err 26 | if (!stats) { 27 | console.log(err) 28 | console.log(chalk.red.bold('\nBuild failed.\n')) 29 | process.exit(1) 30 | } 31 | for (const build of stats.stats) { 32 | if (build.compilation.errors && build.compilation.errors.length) { 33 | hasErrors = true 34 | break 35 | } 36 | } 37 | if (hasErrors) { 38 | console.log(chalk.red.bold('\nBuild failed.\n')) 39 | process.exit(1) 40 | } 41 | const appDirectory = realpathSync(process.cwd()) 42 | const result = spawn.sync( 43 | 'node', 44 | [`${appDirectory}/build/server/server.js`], 45 | { 46 | stdio: 'inherit' 47 | } 48 | ) 49 | process.exit(result.status) 50 | }) 51 | -------------------------------------------------------------------------------- /universal-scripts/lib/universal-config.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { realpathSync } from 'fs' 3 | 4 | const appDirectory = realpathSync(process.cwd()) 5 | 6 | export async function getUniversalConfig(name) { 7 | try { 8 | const config = (await import(join(appDirectory, 'universal.config.mjs'))) 9 | .default 10 | if (typeof config !== 'object') { 11 | console.warn( 12 | "Warning: 'universal.config.mjs' does not export a default object" 13 | ) 14 | return {} 15 | } 16 | return config[name] ?? null 17 | } catch { 18 | return null 19 | } 20 | } 21 | 22 | export async function getPluginsConfig(name) { 23 | try { 24 | const config = await import(join(appDirectory, 'universal.config.mjs')) 25 | console.log({ config }) 26 | const pluginsConfig = config['plugins'] 27 | if (!pluginsConfig) return {} 28 | if (typeof pluginsConfig !== 'object') { 29 | console.warn( 30 | "Warning: 'plugins' in 'universal.config.mjs' does not export an object" 31 | ) 32 | return {} 33 | } 34 | const pluginConfig = pluginsConfig[name] || {} 35 | if (typeof pluginConfig !== 'object') { 36 | console.warn( 37 | `Warning: ${name} plugin in 'universal.config.mjs' does not export an object` 38 | ) 39 | return {} 40 | } 41 | return pluginConfig 42 | } catch { 43 | return {} 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /templates/template-universal-ts/template/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js' 4 | import globals from 'globals' 5 | import tseslint from 'typescript-eslint' 6 | import react from 'eslint-plugin-react' 7 | import eslintPluginPrettier from 'eslint-plugin-prettier/recommended' 8 | import { FlatCompat } from '@eslint/eslintrc' 9 | 10 | const compat = new FlatCompat() 11 | 12 | // import nodePlugin from 'eslint-plugin-n' 13 | 14 | // Waiting to support eslint 9 15 | 16 | // import importPlugin from 'eslint-plugin-import' 17 | // import hooksPlugin from 'eslint-plugin-react-hooks' 18 | 19 | export default [ 20 | eslint.configs.recommended, 21 | // nodePlugin.configs['flat/recommended-script'], 22 | ...tseslint.configs.recommended, 23 | react.configs.flat.recommended, 24 | react.configs.flat['jsx-runtime'], 25 | eslintPluginPrettier, 26 | ...compat.config({ 27 | extends: ['plugin:react-hooks/recommended'] 28 | }), 29 | { 30 | files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], 31 | languageOptions: { 32 | globals: { 33 | ...globals.browser, 34 | ...globals.node, 35 | ...globals.es2022, 36 | __DEV__: 'readonly', 37 | __PROD__: 'readonly', 38 | __SERVER__: 'readonly', 39 | __CLIENT__: 'readonly', 40 | __WATCH__: 'readonly' 41 | } 42 | }, 43 | rules: { 44 | '@typescript-eslint/no-unused-vars': 'warn' 45 | } 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/server/33-lang.tsx: -------------------------------------------------------------------------------- 1 | import { IntlProvider } from 'react-intl' 2 | import { updateIntl } from '../../lib/redux/slices' 3 | import { NextFunction, Request, Response } from 'express' 4 | import { useServerSelector } from '../../lib/redux/selector' 5 | import { ReactNode } from 'react' 6 | // @ts-expect-error Imported from the project 7 | import langs from 'src/locales' 8 | 9 | const addIntl = async (req: Request, res: Response, next: NextFunction) => { 10 | // Determine language 11 | const availableLangs = Object.keys(langs) 12 | let lang = req.acceptsLanguages(availableLangs) || availableLangs[0] 13 | const forceLang = req.cookies['lang'] 14 | if (forceLang && availableLangs.indexOf(forceLang) >= 0) lang = forceLang 15 | 16 | // Set it 17 | if (req.store) req.store.dispatch(updateIntl({ lang, messages: langs[lang] })) 18 | 19 | return await next() 20 | } 21 | 22 | export const serverMiddleware = addIntl 23 | 24 | const ReduxIntlProvider = ({ children }: { children: ReactNode }) => { 25 | const intl = useServerSelector((s) => s.intl) 26 | return ( 27 | 28 | {children} 29 | 30 | ) 31 | } 32 | 33 | const renderIntlProvider = async ( 34 | req: Request, 35 | res: Response, 36 | next: () => Promise 37 | ) => {await next()} 38 | 39 | export const reactRoot = renderIntlProvider 40 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/client/33-lang.tsx: -------------------------------------------------------------------------------- 1 | import { IntlProvider } from 'react-intl' 2 | import { updateIntl } from '../../lib/redux/slices' 3 | import { useAppSelector } from '../../lib/redux/selector' 4 | import { ClientInit, ClientRoot } from '../../lib/redux/types' 5 | import { ReactNode } from 'react' 6 | // @ts-expect-error Imported from the project 7 | import langs from 'src/locales' 8 | 9 | const addClientIntl: ClientInit = (ctx, next) => { 10 | // Determine language 11 | const availableLangs = Object.keys(langs) 12 | const storeIntl = ctx.store && ctx.store.getState().intl 13 | let lang = storeIntl && storeIntl.lang 14 | 15 | if (!lang) { 16 | lang = document.documentElement.lang || window.navigator.language 17 | } 18 | lang = availableLangs.indexOf(lang) !== -1 ? lang : availableLangs[0] 19 | 20 | // Set it 21 | if (ctx.store) ctx.store.dispatch(updateIntl({ lang, messages: langs[lang] })) 22 | 23 | return next() 24 | } 25 | 26 | export const clientInit = addClientIntl 27 | 28 | const ReduxIntlProvider = ({ children }: { children: ReactNode }) => { 29 | const intl = useAppSelector((s) => s.intl) 30 | return ( 31 | 32 | {children} 33 | 34 | ) 35 | } 36 | 37 | const renderIntlProvider: ClientRoot = async (ctx, next) => ( 38 | {await next()} 39 | ) 40 | 41 | export const reactRoot = renderIntlProvider 42 | -------------------------------------------------------------------------------- /universal-scripts/build.conf.d/15-optimize.js: -------------------------------------------------------------------------------- 1 | const optimization = { 2 | cacheGroups: { 3 | vendor: { 4 | test: /[\\/]node_modules[\\/]/, 5 | priority: -5, 6 | name: 'vendors', 7 | chunks: 'initial', 8 | reuseExistingChunk: true, 9 | minSize: 0 10 | }, 11 | default: { 12 | minChunks: 2, 13 | priority: -20, 14 | reuseExistingChunk: true 15 | }, 16 | defaultVendors: false, 17 | reactPackage: { 18 | test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/, 19 | name: 'vendor_react', 20 | chunks: 'all', 21 | priority: 10 22 | } 23 | } 24 | } 25 | 26 | const enhancer = (opts = {}, config) => { 27 | const isWatch = opts.isWatch 28 | 29 | if (opts.id === 'server') { 30 | if (!isWatch) { 31 | config.optimization.splitChunks = optimization 32 | } else { 33 | config.optimization.splitChunks = { 34 | cacheGroups: { 35 | vendor: { 36 | test: /[\\/]node_modules[\\/]/, 37 | name: 'vendors', 38 | reuseExistingChunk: true 39 | } 40 | } 41 | } 42 | } 43 | 44 | return config 45 | } 46 | 47 | if (opts.id === 'client') { 48 | config.optimization.splitChunks = optimization 49 | if (isWatch) { 50 | config.optimization.runtimeChunk = 'single' 51 | } 52 | 53 | return config 54 | } 55 | 56 | return config 57 | } 58 | 59 | export const webpack = enhancer 60 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/server/03-custom-error.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { Request, Response } from 'express' 3 | import fs from 'node:fs' 4 | 5 | // Optional error 500 page 6 | const customError500 = 7 | __SSR__ && 8 | (() => { 9 | const req = require.context('src/routes', false, /^\.\/index$/) 10 | const keys = req(req.keys()[0]) 11 | if (keys.error500 && fs.existsSync(keys.error500)) { 12 | return fs.readFileSync(keys.error500, 'utf-8') 13 | } 14 | })() 15 | 16 | const handleErrors = async (err: Error, req: Request, res: Response) => { 17 | // Loguea el error con la pila 18 | console.error(chalk.red('Error during render:\n') + err.stack) 19 | 20 | // Establece el estado HTTP a 500 (Internal Server Error) 21 | res.status(500) 22 | 23 | // Si existe una página de error personalizada (customError500), la usamos 24 | if (customError500) { 25 | res.send(customError500) 26 | } 27 | // Si estamos en un entorno de desarrollo (__DEV__), mostrar el stack del error 28 | else if (__DEV__) { 29 | res.send( 30 | '

Internal Server Error

\n' + 31 | '

An exception was caught during page rendering:

\n' + 32 | '
' +
33 |         err.stack +
34 |         '
' 35 | ) 36 | } 37 | // Si no hay una página personalizada ni estamos en desarrollo, mostrar un mensaje genérico 38 | else { 39 | res.send('

Internal Server Error

') 40 | } 41 | } 42 | 43 | export const serverErrorMiddleware = handleErrors 44 | -------------------------------------------------------------------------------- /universal-scripts/docs/deploying.md: -------------------------------------------------------------------------------- 1 | ## Deploying to a platform 2 | 3 | Deploying an Universal Scripts app is very easy on most platforms. 4 | 5 | ### Heroku 6 | 7 | The project is already configured for deployiment at Heroku and any other system based on their buildpacks, like Dokku. Just push your code to the server, and it will generate a build and serve it. 8 | 9 | ### Other platforms 10 | 11 | If you use any other platform, just configure it so that Node is available, and run the following commands: 12 | 13 | - Before build: npm install 14 | - Build: npm run build 15 | - Execute: npm run serve 16 | 17 | ### Static build 18 | 19 | Generating a static build to be served with a static file server is not officially supported, as you'd lose the server-side rendering benefits, but if you still want to do it, the process would be something like: 20 | 21 | - Generate a standard build (ie: `npm run build`) 22 | - Create a index.html which loads the JS and CSS assets listed at `build/client/webpack-chunks.json` 23 | - Configure your web server to serve the `build/client` folder, and reply to all unknown routes with the index.htm file. 24 | 25 | The generated code will compensate for the missing initial state and DOM nodes, and the page will work. 26 | 27 | ### Subdirectory 28 | 29 | The build system allows configuring a build (or watch mode) to run inside a subdirectory. Just set the ENV var `SUBDIRECTORY` to the absolute path of the app subdirectory (ie: `/client/`). Relative URLs are unsupported, as they would change meaning depending on which route gets loaded. 30 | -------------------------------------------------------------------------------- /universal-scripts/docs/runtime-system.md: -------------------------------------------------------------------------------- 1 | ## The runtime system 2 | 3 | When the bundles get run on the client or the server, they perform a lot of tasks to render your application. 4 | 5 | ### Runtime config 6 | 7 | The runtime works similarly to the build system, using [js.conf.d-webpack](https://www.npmjs.com/package/js.conf.d-webpack) for modularity and ease of customization. The folder to add or override pieces of the runtime is `src/runtime.conf.d`, and you can create subfolders for `client` or `server` if you need them to run different code. 8 | 9 | To learn more about the pieces that are included by default, you can check the `runtime.conf.d` folder on this project's root. 10 | 11 | ### Hooks 12 | 13 | To determine when and how to call each runtime piece, we use a hook system. To use it, each file can have some special exports that get called on different moments of the application lifecycle. If multiple files export a hook, they will be used on file priority order. Hooks follow the Koa middleware spec, receiving a shared context, and a next function pointer. 14 | 15 | The following hooks are available: 16 | 17 | - `serverMiddleware`: (Server only) Adds a middleware to the internal Koa server. 18 | - `clientInit`: (Client only) Gets called as part of the initialization process. 19 | - `reactRoot`: Builds the root component for React, allowing multiple wrappers to be added to it. 20 | 21 | The `33-lang` module is a good example which uses all hooks, to learn more about them. 22 | 23 | ## Next steps 24 | 25 | After learning about the runtime system, we're ready for the last step: [deploying to a platform](deploying). 26 | -------------------------------------------------------------------------------- /universal-scripts/docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import '{{ site.theme }}'; 5 | 6 | aside.menu { 7 | background-color: #2a2a2a; 8 | bottom: 0; 9 | left: 0; 10 | padding: 10px; 11 | position: fixed; 12 | top: 64px; 13 | width: 200px; 14 | box-sizing: border-box; 15 | } 16 | 17 | #header .header-about { 18 | float: left; 19 | } 20 | 21 | #header .header-about .title { 22 | color: #e8e8e8; 23 | font-size: 18px; 24 | } 25 | 26 | #header nav li.fork { 27 | float: right; 28 | } 29 | 30 | #header nav { 31 | background: transparent; 32 | } 33 | 34 | code.highlighter-rouge { 35 | background-color: #191919; 36 | border: 1px solid #5a4e1c; 37 | margin: 0; 38 | padding: 2px; 39 | } 40 | 41 | /* Mobile fixes */ 42 | @media print, screen and (max-width: 640px) { 43 | aside.menu { 44 | position: static; 45 | height: auto; 46 | margin-top: 40px; 47 | width: 100%; 48 | } 49 | aside.menu ul { 50 | margin: 0; 51 | } 52 | section { 53 | margin-top: 0; 54 | padding-top: 0; 55 | } 56 | section h2 { 57 | margin-top: 20px; 58 | } 59 | #header { 60 | margin-top: 0; 61 | height: 40px; 62 | } 63 | #header .header-about { 64 | float: none; 65 | } 66 | nav .title { 67 | text-align: center; 68 | } 69 | nav { 70 | display: block; 71 | } 72 | nav .subtitle { 73 | display: none; 74 | } 75 | nav li.fork { 76 | display: none !important; 77 | } 78 | } 79 | 80 | /* Other responsive fixes */ 81 | @media print, screen and (min-width: 641px) and (max-width: 1134px) { 82 | .wrapper { 83 | margin-left: 220px; 84 | margin-right: 20px; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /universal-scripts/lib/redux/slices.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | import { cleanup } from './actions' 3 | import { IncomingHttpHeaders } from 'node:http' 4 | 5 | interface ReqState { 6 | headers: IncomingHttpHeaders 7 | origin: string 8 | path: string 9 | ip: string 10 | cookies: Record 11 | } 12 | 13 | const initialReqState: ReqState = null 14 | 15 | const requestSlice = createSlice({ 16 | name: 'request', 17 | initialState: initialReqState, 18 | reducers: { 19 | requestInit(_state, action: PayloadAction) { 20 | return action.payload 21 | } 22 | }, 23 | extraReducers(builder) { 24 | builder.addCase(cleanup, () => { 25 | return null 26 | }) 27 | } 28 | }) 29 | 30 | export const { requestInit } = requestSlice.actions 31 | export const requestReducer = requestSlice.reducer 32 | 33 | interface IntlState { 34 | lang: string 35 | messages: Record 36 | } 37 | 38 | const initialIntlState: IntlState = { 39 | lang: 'en', 40 | messages: {} 41 | } 42 | 43 | const intlSlice = createSlice({ 44 | name: 'intl', 45 | initialState: initialIntlState, 46 | reducers: { 47 | updateIntl(state, action: PayloadAction) { 48 | return action.payload 49 | } 50 | }, 51 | extraReducers(builder) { 52 | builder.addCase(cleanup, (state) => { 53 | return { lang: state.lang, messages: {} } 54 | }) 55 | builder.addDefaultCase((state) => { 56 | return state || { lang: 'en', messages: {} } 57 | }) 58 | } 59 | }) 60 | 61 | export const { updateIntl } = intlSlice.actions 62 | export const intlReducer = intlSlice.reducer 63 | -------------------------------------------------------------------------------- /universal-scripts/lib/find-scripts.js: -------------------------------------------------------------------------------- 1 | import { resolve, join } from 'path' 2 | import fs from 'fs' 3 | 4 | const appDirectory = fs.realpathSync(process.cwd()) 5 | 6 | const regex = /^(@[^/]+\/)?universal-plugin[^/]+$/ 7 | 8 | export function findUniversalPlugins() { 9 | const nodeModulesPath = resolve(appDirectory, 'node_modules') 10 | 11 | const folders = fs.readdirSync(nodeModulesPath, { 12 | withFileTypes: true 13 | }) 14 | 15 | let universalPlugins = folders 16 | .filter((dirent) => regex.test(dirent.name)) 17 | .map((dirent) => join(nodeModulesPath, dirent.name)) 18 | 19 | const namespaces = folders.filter( 20 | (dirent) => dirent.isDirectory() && dirent.name.startsWith('@') 21 | ) 22 | 23 | namespaces.forEach((namespace) => { 24 | const namespacePath = join(nodeModulesPath, namespace.name) 25 | const subFolders = fs 26 | .readdirSync(namespacePath, { withFileTypes: true }) 27 | .filter((dirent) => regex.test(`${namespace.name}/${dirent.name}`)) 28 | .map((dirent) => join(namespacePath, dirent.name)) 29 | 30 | universalPlugins = [...universalPlugins, ...subFolders] 31 | }) 32 | 33 | return universalPlugins 34 | } 35 | 36 | export function filterPluginsWithSubdir(plugins, subdir) { 37 | return plugins.filter((pluginPath) => { 38 | const testDirPath = join(pluginPath, subdir) 39 | return fs.existsSync(testDirPath) && fs.statSync(testDirPath).isDirectory() 40 | }) 41 | } 42 | 43 | export function findScriptInPlugin(plugins, script) { 44 | const pluginRoute = plugins.find((pluginPath) => { 45 | const testDirPath = join(pluginPath, 'scripts', `${script}.js`) 46 | return fs.existsSync(testDirPath) 47 | }) 48 | 49 | return `${pluginRoute}/scripts/${script}.js` 50 | } 51 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/server/50-render.tsx: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express' 2 | import { ReactNode } from 'react' 3 | import { renderToPipeableStream } from 'react-dom/server' 4 | 5 | // Optional error 500 page 6 | const ErrorHandler = 7 | __SSR__ && 8 | (() => { 9 | const req = require.context('src/routes', false, /^\.\/index$/) 10 | const keys = req(req.keys()[0]) 11 | return keys.ErrorHandler 12 | })() 13 | 14 | const render = (req: Request, res: Response, root: ReactNode): Promise => 15 | new Promise((resolve, reject) => { 16 | const stream = renderToPipeableStream(root, { 17 | bootstrapScripts: req.assets?.scripts, 18 | onAllReady: () => { 19 | if (req.renderCtx && req.renderCtx.url) { 20 | // There was a redirect 21 | res.redirect(req.renderCtx.url) 22 | } else { 23 | res.set('Content-Type', 'text/html; charset=utf-8') 24 | req.stream = stream 25 | } 26 | resolve() 27 | }, 28 | onError: (e) => { 29 | reject(e) 30 | } 31 | }) 32 | }) 33 | 34 | const renderMiddleware = async ( 35 | req: Request, 36 | res: Response, 37 | next: NextFunction 38 | ) => { 39 | // Run any other middlewares 40 | await next() 41 | 42 | // Run the render hook to get the root element 43 | const children = await req.triggerHook('reactRoot')(req, res, false) 44 | 45 | // Actual rendering 46 | try { 47 | await render(req, res, children) 48 | } catch (err) { 49 | if (ErrorHandler) { 50 | await render(req, res, ) 51 | } else { 52 | await render(req, res, false) 53 | } 54 | res.status(err.status || 500) 55 | } 56 | } 57 | 58 | export const serverMiddleware = renderMiddleware 59 | -------------------------------------------------------------------------------- /universal-scripts/docs/store.md: -------------------------------------------------------------------------------- 1 | ## Redux Store 2 | 3 | Your project is preconfigured with a Redux store, available both at the server and the client. 4 | 5 | ### Reducers 6 | 7 | The store has a [combineReducers](https://redux.js.org/api/combinereducers/) as the root reducer, and you can add your reducers to it by exporting an object from `src/store/reducers.js`. 8 | 9 | In addittion to your reducers, there are a few already preconfigured: 10 | 11 | - `intl`: Your locale settings, managed by [react-intl-redux](https://www.npmjs.com/package/react-intl-redux) 12 | - `useFetch`: The request cache for [ruse-fetch](https://www.npmjs.com/package/ruse-fetch) 13 | - `req`: (server only) Info about the request being rendered, such as the source ip or headers 14 | 15 | You can override them by creating a key with the same name, but things can fail if you break their contract. 16 | 17 | ### Middlewares 18 | 19 | If you need to add any extra Redux middlewares to the store, you can create `src/store/middlewares.js` and export an array of middlewares to configure them. The [thunk](https://github.com/reduxjs/redux-thunk) middleware will be added automatically. 20 | 21 | ### Actions 22 | 23 | There are some actions that get dispatched during various stages of the render process, which can be used on your reducers: 24 | 25 | - On the server: 26 | - `REQUEST_INIT`: Dispatched before starting rendering. Contains info about the request being rendered. 27 | - `CLEANUP`: Dispatched after render, but before serializing the store. Allows cleaning up any info that should not be serialized and sent to the client. 28 | - On the client 29 | - `CLIENT_INIT`: Dispatched after store init, but before render. Allows middlewares to update the store before the first client render. 30 | 31 | You can import their action types like this: 32 | 33 | ```javascript 34 | import { CLEANUP } from 'universal-scripts' 35 | ``` 36 | 37 | ## Next steps 38 | 39 | Now that you know how the store works, you can learn more about the [build system](build-system). 40 | -------------------------------------------------------------------------------- /universal-scripts/docs/index.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | _Universal Scripts_ is an alternative configuration for [Create React App](https://github.com/facebookincubator/create-react-app), designed for production sites that need features such as: 4 | 5 | - Server-Side Rendering (aka. _universal rendering_) 6 | - Internationalization with `react-intl` 7 | - HTML head customization with `react-helmet-async` 8 | - Redux state container with `redux-toolkit` 9 | - Sass (and SCSS) support 10 | - And of course, everything on React Scripts: ES6 & TypeScript support, React Router, etc. 11 | 12 | Everything on a single package, easy to keep updated, and ready to deploy to your favourite platform. 13 | 14 | ## But... why? 15 | 16 | When learning, Create React App is a nice way to kickstart your projects, as you can forget about handing Webpack and Babel configs, dependency updates, etc., but the defaults are designed for an easy learning experience, at the expense of some features that are needed in a modern web application. 17 | 18 | Server side rendering is a must for a production site, so your visitors can get the content much faster instead of waiting looking at an empty screen while your JS downloads and processes. It also helps make the page more accesible to crawlers and other tools that don't yet understand JS-only sites, like most Opengraph extractors. 19 | 20 | Internationalization is needed on any site targeting a broad audience from multiple countries, but it is useful even for single-language pages, as it helps keeping strings organized, and number and date formatting consistent. 21 | 22 | [Redux](https://redux.js.org/) helps keep state organized and predictable, and is a great fit when working with universal rendering, as it allows easy state serialization to send initial content to the client. 23 | 24 | And we also included a few other goodies, like [Sass](https://sass-lang.com/), the best known CSS extension language, or [react-helmet-async](https://github.com/staylor/react-helmet-async), a way of managing the document head. 25 | 26 | ## Sounds good? 27 | 28 | Head to the [Getting Started](getting-started) section to try it. 29 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-jest/build.conf.d/05-base-test.js: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'path' 2 | import { realpathSync, readFileSync, existsSync } from 'fs' 3 | import { fileURLToPath } from 'url' 4 | import jest from 'jest' 5 | 6 | const __filename = fileURLToPath(import.meta.url) 7 | const __dirname = dirname(__filename) 8 | 9 | const appDirectory = realpathSync(process.cwd()) 10 | 11 | const getAliasesFromTSConfig = () => { 12 | const tsconfigPath = resolve(appDirectory, 'tsconfig.json') 13 | 14 | if (!existsSync(tsconfigPath)) return {} 15 | 16 | const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf-8')) 17 | const paths = tsconfig.compilerOptions?.paths || {} 18 | 19 | const aliases = {} 20 | for (const [key, value] of Object.entries(paths)) { 21 | const cleanedPath = value[0].replace(/^\.\/?/, '') 22 | 23 | // Convertir a formato compatible con Jest 24 | const aliasKey = `^${key.replace('/*', '(.*)')}$` 25 | const aliasValue = `/${cleanedPath.replace('/*', '$1')}` 26 | aliases[aliasKey] = aliasValue 27 | } 28 | 29 | return aliases 30 | } 31 | 32 | const enhancer = () => { 33 | const config = { 34 | roots: ['/src'], 35 | moduleDirectories: ['node_modules', ''], 36 | transform: { 37 | '^.+\\.(js|jsx|ts|tsx)$': resolve(__dirname, '../lib/swcTransform') 38 | }, 39 | transformIgnorePatterns: [ 40 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$' 41 | ], 42 | moduleNameMapper: { 43 | '\\.(css|sass|scss)$': resolve(__dirname, '../lib/styleMock.js'), 44 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 45 | resolve(__dirname, '../lib/fileMock.js'), 46 | ...getAliasesFromTSConfig() 47 | }, 48 | setupFilesAfterEnv: [] 49 | } 50 | 51 | const setupFiles = ['src/setupTests.ts', 'src/setupTests.js'] 52 | setupFiles.forEach((f) => { 53 | if (existsSync(f)) config.setupFilesAfterEnv.push('/' + f) 54 | }) 55 | 56 | const args = [`--config=${JSON.stringify(config)}`] 57 | 58 | return () => jest.run(args) 59 | } 60 | 61 | export const test = enhancer 62 | -------------------------------------------------------------------------------- /universal-scripts/docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% seo %} 8 | 9 | 10 | 11 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 31 | 32 | 47 | 48 |
49 | 50 |
51 | 52 | {{ content }} 53 | 54 |
55 | 56 |
57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /universal-scripts/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Getting started 2 | 3 | Creating a Universal Scripts project is very easy. 4 | Using [npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b), just do: 5 | 6 | > npx create-react-app \-\-scripts-version universal-scripts <_app-name_> 7 | 8 | When it finishes you'll have a project ready to start developing. 9 | Enter the project folder and run: 10 | 11 | > npm start 12 | 13 | It will start a development server on _localhost:3000_, watching for changes in any files on your project, and live-reloading them, similar to other Create React App projects. 14 | 15 | ## Folder structure 16 | 17 | Your new project already contains some predefined folders: 18 | 19 | - `src/locales`: Your app translations. The first key on the index will be the default language. 20 | - `src/routes`: The index will be the root component of your app. Use react-router to manage routing. 21 | - `src/static`: Add your static assets (images, fonts, etc.), and they will get copied to the output root. 22 | - `src/store`: Home of your Redux actions and reducers. At `reducers.js` you should export an object to pass to combineReducers to build the root reducer. 23 | - `src/styles`: Your stylesheets get loaded from the index.sass, so import them from there. 24 | 25 | ## TypeScript 26 | 27 | If you would rather use TypeScript, you can create your project using our alternate TypeScript template, by doing: 28 | 29 | > npx create-react-app \-\-scripts-version universal-scripts \-\-template universal-ts <_app-name_> 30 | 31 | All the config and structure is exactly the same, but using TypeScript. 32 | 33 | ## Custom templates 34 | 35 | Just like stock Create React App, we support the use of custom templates during project init. 36 | 37 | Not all CRA templates will be compatible, though: they must contain the required entrypoints provided in our base template, [cra-template-universal](https://github.com/GlueDigital/cra-template-universal). If you want to create your custom template, we recommend using it as a starting point. 38 | 39 | ## Next steps 40 | 41 | Now let's see how to handle [data fetching](data-fetching) both on the client and the server. 42 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/server/30-redux.tsx: -------------------------------------------------------------------------------- 1 | import { createServerStore } from '../../lib/redux/store' 2 | import { Provider } from 'react-redux' 3 | import { cleanup } from '../../lib/redux/actions' 4 | import { requestInit } from '../../lib/redux/slices' 5 | import jsesc from 'jsesc' 6 | import { NextFunction, Request, Response } from 'express' 7 | import { ReactNode } from 'react' 8 | 9 | const addRedux = async (req: Request, res: Response, next: NextFunction) => { 10 | const store = createServerStore() 11 | 12 | store.dispatch( 13 | requestInit({ 14 | headers: req.headers, 15 | origin: req.get('origin'), 16 | path: req.path, 17 | ip: req.ip, 18 | cookies: parseCookies(req.headers.cookie), 19 | ...req.initExtras // Allow passing in data from previous middlewares 20 | }) 21 | ) 22 | 23 | // Make it available through the context 24 | req.store = store 25 | 26 | // Run any other middlewares 27 | await next() 28 | 29 | // Clean up the resulting state 30 | store.dispatch(cleanup()) 31 | const state = store.getState() 32 | const copyState = structuredClone(state) 33 | delete copyState.req // This reducer doesn't exist client-side 34 | 35 | // Send store contents along the page 36 | const storeOutput = jsesc(copyState, { isScriptContext: true }) 37 | const envVariables = Object.fromEntries( 38 | Object.entries(process.env).filter(([key]) => key.startsWith('PUBLIC_')) 39 | ) 40 | req.assets?.styles.unshift( 41 | `` 42 | ) 43 | req.assets?.styles.unshift( 44 | '' 45 | ) 46 | } 47 | 48 | const parseCookies = (s) => 49 | !s 50 | ? {} 51 | : s 52 | .split(';') 53 | .map((v) => v.split('=')) 54 | .reduce((acc, v) => { 55 | acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim()) 56 | return acc 57 | }, {}) 58 | 59 | export const serverMiddleware = addRedux 60 | 61 | const renderIntlProvider = async ( 62 | req: Request, 63 | res: Response, 64 | next: () => Promise 65 | ) => {await next()} 66 | 67 | export const reactRoot = renderIntlProvider 68 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-helmet/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | test 4 | 5 | 6 | test 7 | 8 | 9 |

10 |

11 | 12 | npm downloads 13 | 14 | 15 | node current 16 | 17 |

18 |

19 | 20 | node current 21 | 22 | 23 | node current 24 | 25 |

26 | 27 | # Universal Plugin Helmet 28 | 29 | This is a plugin for [`universal-scripts`](https://github.com/GlueDigital/universal-scripts/tree/master/universal-scripts) framework. It provides seamless integration with [`react-helmet-async`](https://github.com/staylor/react-helmet-async), enabling dynamic and efficient management of metadata in React applications. 30 | 31 | ## Features 32 | 33 | - Integrates `react-helmet-async` for improved performance in server-side rendering (SSR). 34 | - Allows dynamic updates to `` elements, such as titles, meta tags, and Open Graph data. 35 | - Enhances SEO and accessibility by enabling proper metadata management. 36 | - Supports overriding and extending configurations within Universal’s modular setup. 37 | 38 | ## Installation 39 | 40 | To install the plugin, run: 41 | 42 | ```sh 43 | yarn add @gluedigital/universal-plugin-helmet 44 | ``` 45 | 46 | If using npm 47 | 48 | ```sh 49 | npm install @gluedigital/universal-plugin-helmet 50 | ``` 51 | -------------------------------------------------------------------------------- /universal-scripts/build.conf.d/10-entrypoints.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import CopyWebpackPlugin from 'copy-webpack-plugin' 4 | import webpackPackage from 'webpack' 5 | import { fileURLToPath } from 'url' 6 | 7 | const { HotModuleReplacementPlugin } = webpackPackage 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = path.dirname(__filename) 11 | 12 | const appDirectory = fs.realpathSync(process.cwd()) 13 | 14 | const enhancer = (opts = {}, config) => { 15 | const isWatch = opts.isWatch 16 | 17 | if (opts.id === 'server') { 18 | // For the in-memory server side HMR, we need to run the server outside 19 | // of the build, as it will contain the dev server, and do HMR for the part 20 | // which is built. 21 | // But when doing a static build, we want the entire server on the output. 22 | const serverPath = path.resolve(__dirname, '..', 'server') 23 | if (isWatch) { 24 | config.entry = { 25 | server: [path.resolve(serverPath, 'serverMiddleware')] 26 | } 27 | config.output.libraryTarget = 'commonjs2' 28 | } else { 29 | config.entry = { 30 | server: [path.resolve(serverPath, 'main')] 31 | } 32 | } 33 | 34 | return config 35 | } 36 | 37 | if (opts.id === 'client') { 38 | // Add our render entrypoint 39 | config.entry = { 40 | main: [path.resolve(__dirname, '..', 'client', 'init')] 41 | } 42 | 43 | if (!isWatch) { 44 | // Copy static assets to output dir 45 | config.plugins.push( 46 | new CopyWebpackPlugin({ 47 | patterns: [ 48 | { 49 | from: path.resolve(appDirectory, 'src', 'static'), 50 | to: path.resolve(appDirectory, 'build', 'client') 51 | } 52 | ] 53 | }) 54 | ) 55 | } else { 56 | config.plugins.push(new HotModuleReplacementPlugin()) 57 | config.entry.main.push( 58 | 'webpack-hot-middleware/client?reload=true&noInfo=true' 59 | ) 60 | } 61 | return config 62 | } 63 | 64 | // For other extraneous builds, add some sane defaults, 65 | // even if they will most likely be overriden 66 | config.entry = { 67 | main: [path.resolve(appDirectory, 'src', opts.id)] 68 | } 69 | 70 | return config 71 | } 72 | 73 | export const webpack = enhancer 74 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-ssg/runtime.conf.d/server/40-ssg.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { Server } from 'http' 3 | import { AddressInfo } from 'net' 4 | import { resolve, join, dirname, basename } from 'path' 5 | 6 | const makeRequests = async (port: number) => { 7 | let routes = [] 8 | 9 | try { 10 | // @ts-expect-error Resolved in runtime 11 | const { getStaticRoutes } = await import('src/routes/static-routes.mjs') 12 | const result = await getStaticRoutes() 13 | 14 | if (!Array.isArray(result)) { 15 | throw new Error('getStaticRoutes() did not return an array') 16 | } 17 | 18 | routes = result.filter((route) => route && typeof route === 'string') 19 | console.log('Routes loaded:', routes) 20 | } catch (error) { 21 | console.warn('Error loading static routes:', error) 22 | } 23 | 24 | const pathStaticSites = resolve(process.cwd(), 'static-sites') 25 | 26 | try { 27 | if (!fs.existsSync(pathStaticSites)) { 28 | fs.mkdirSync(pathStaticSites, { recursive: true }) 29 | } 30 | } catch (error) { 31 | console.error('Error creating static-sites directory:', error) 32 | return 33 | } 34 | 35 | for (const route of routes) { 36 | try { 37 | const response = await fetch(`http://localhost:${port}/${route}`) 38 | 39 | if (!response.ok) { 40 | throw new Error( 41 | `Failed to fetch http://localhost:${port}/${route}: ${response.statusText}` 42 | ) 43 | } 44 | 45 | const data = await response.text() 46 | 47 | const safePath = route.replace(/^\/|\/$/g, '') || 'index' 48 | 49 | const fullPath = join(pathStaticSites, safePath) 50 | const dirPath = dirname(fullPath) 51 | const fileName = `${basename(fullPath)}.html` 52 | 53 | fs.mkdirSync(dirPath, { recursive: true }) 54 | 55 | fs.writeFileSync(join(dirPath, fileName), data, { 56 | encoding: 'utf-8' 57 | }) 58 | 59 | console.log(`✅ Successfully saved ${fileName}`) 60 | } catch (error) { 61 | console.error(`❌ Error requesting ${route}:`, error) 62 | } 63 | } 64 | } 65 | 66 | export const appAfter = (server: Server) => { 67 | if ( 68 | typeof __SSG__ === 'undefined' || 69 | (typeof __SSG__ === 'boolean' && !__SSG__) 70 | ) 71 | return 72 | const port = (server.address() as AddressInfo).port 73 | makeRequests(port).then(() => { 74 | server.close() 75 | process.exit(0) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-sass/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | test 4 | 5 | 6 | test 7 | 8 | 9 |

10 |

11 | 12 | npm downloads 13 | 14 | 15 | node current 16 | 17 |

18 |

19 | 20 | node current 21 | 22 | 23 | node current 24 | 25 |

26 | 27 | # Universal Plugin Sass 28 | 29 | This is a plugin for the [`universal-scripts`](https://github.com/GlueDigital/universal-scripts/tree/master/universal-scripts) framework. It enables support for [`Sass`](https://sass-lang.com/), allowing you to use `.scss` or `.sass` files in your Universal project. 30 | 31 | ## Features 32 | 33 | - Enables Sass (`.scss` and `.sass` file support) for styling. 34 | - Compatible with CSS Modules for scoped styling. 35 | - Works with Webpack-based builds in Universal projects. 36 | - Allows better organization of styles and reusable variables. 37 | 38 | ## Installation 39 | 40 | To install the plugin, run: 41 | 42 | ```sh 43 | yarn add @gluedigital/universal-plugin-sass 44 | ``` 45 | 46 | If using npm 47 | 48 | ```sh 49 | npm install @gluedigital/universal-plugin-sass 50 | ``` 51 | 52 | Then you can 53 | 54 | ```{sass} 55 | // styles/index.sass 56 | body 57 | background-color: #f4f4f4 58 | font-family: Arial, sans-serif 59 | ``` 60 | 61 | Import it in your application: 62 | 63 | ```typescript 64 | import "./styles/index.sass" 65 | 66 | const App = () =>
Welcome to My App
67 | 68 | export default App 69 | ``` 70 | -------------------------------------------------------------------------------- /universal-scripts/lib/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | combineReducers, 3 | configureStore, 4 | ReducersMapObject 5 | } from '@reduxjs/toolkit' 6 | import { fetchReducer } from 'ruse-fetch' 7 | import { intlReducer, requestReducer } from './slices' 8 | 9 | // @ts-expect-error Imported from the project 10 | import reducerList from 'src/store/reducers' 11 | 12 | const addClientAutoReducers = (userReducers: ReducersMapObject) => { 13 | const autoReducers = { 14 | intl: intlReducer, 15 | useFetch: fetchReducer 16 | } 17 | return combineReducers({ ...autoReducers, ...userReducers }) 18 | } 19 | 20 | const addServerAutoReducers = (userReducers: ReducersMapObject) => { 21 | const autoReducers = { 22 | intl: intlReducer, 23 | useFetch: fetchReducer, 24 | req: requestReducer 25 | } 26 | return combineReducers({ ...autoReducers, ...userReducers }) 27 | } 28 | 29 | // Optional extra middlewares 30 | const extraMiddlewares = (() => { 31 | const req = require.context('src/store', false, /^\.\/middlewares$/) 32 | if (req.keys().length) return req(req.keys()[0]).default 33 | return [] 34 | })() 35 | 36 | const createStore = ( 37 | reducers: ReducersMapObject, 38 | initialState: Record | undefined = undefined, 39 | isServer: boolean 40 | ) => { 41 | const autoReducers = isServer 42 | ? addServerAutoReducers(reducers) 43 | : addClientAutoReducers(reducers) 44 | 45 | const store = configureStore({ 46 | reducer: autoReducers, 47 | middleware: (getDefaultMiddleware) => 48 | getDefaultMiddleware({ 49 | serializableCheck: { 50 | ignoredPaths: ['useFetch'], 51 | ignoredActions: [ 52 | 'useFetch/fetchError', 53 | 'useFetch/fetchLoading', 54 | 'useFetch/fetchSuccess', 55 | 'useFetch/fetchCleanup', 56 | 'useFetch/fetchUnuse', 57 | 'useFetch/fetchUse' 58 | ] 59 | } 60 | }).concat(extraMiddlewares), 61 | preloadedState: initialState 62 | }) 63 | 64 | // Hot Module Replacement (HMR) 65 | if (module.hot) { 66 | module.hot.accept('src/store/reducers', async () => { 67 | // @ts-expect-error Imported from the project 68 | const updatedReducers = (await import('src/store/reducers')).default 69 | const newAutoReducers = isServer 70 | ? addServerAutoReducers(updatedReducers) 71 | : addClientAutoReducers(updatedReducers) 72 | store.replaceReducer(newAutoReducers) 73 | }) 74 | } 75 | 76 | return store 77 | } 78 | 79 | export const createServerStore = () => createStore(reducerList, undefined, true) 80 | 81 | export const createClientStore = (initialState: Record) => 82 | createStore(reducerList, initialState, false) 83 | -------------------------------------------------------------------------------- /universal-scripts/build.conf.d/95-gen-index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a index.html file on client build if SSR is disabled. 3 | */ 4 | import fs from 'fs' 5 | import path from 'path' 6 | import webpackPkg from 'webpack' 7 | import { getUniversalConfig } from '../lib/universal-config.js' 8 | const { sources } = webpackPkg 9 | 10 | const appDirectory = fs.realpathSync(process.cwd()) 11 | 12 | const noSsr = await getUniversalConfig('noSsr') 13 | 14 | const ssr = noSsr == null ? true : !noSsr 15 | 16 | // The default template can be overriden at src/static/index.html 17 | const defaultTemplate = 18 | '' + 19 | '' + 20 | '' + 21 | '' + 22 | '' + 23 | '' + 24 | '' + 25 | '' + 26 | '
' + 27 | '' + 28 | '' + 29 | '' + 30 | '' 31 | 32 | // The webpack plugin which generates the index 33 | class GenIndexPlugin { 34 | apply(compiler) { 35 | this.publicPath = compiler.options.output.publicPath 36 | compiler.hooks.emit.tap('genindex', this.genIndex.bind(this)) 37 | } 38 | 39 | genIndex(compilation) { 40 | const publicPath = this.publicPath 41 | const assets = Object.keys(compilation.assets) 42 | 43 | let template = defaultTemplate 44 | const templateOverridePath = path.join( 45 | appDirectory, 46 | 'src', 47 | 'static', 48 | 'index.html' 49 | ) 50 | if (fs.existsSync(templateOverridePath)) { 51 | template = fs.readFileSync(templateOverridePath).toString() 52 | } 53 | 54 | const scripts = [] 55 | const styles = [] 56 | for (const asset of assets) { 57 | if (asset.endsWith('.js') && asset !== 'polyfills.js') scripts.push(asset) 58 | if (asset.endsWith('.css')) styles.push(asset) 59 | } 60 | 61 | const scriptsFragment = scripts 62 | .map((script) => ``) 63 | .join('') 64 | const stylesFragment = styles 65 | .map((style) => ``) 66 | .join('') 67 | 68 | const index = template 69 | .replace('', scriptsFragment) 70 | .replace('', stylesFragment) 71 | 72 | // THIS SHOULB BE BEFORE COMPILATION 73 | compilation.emitAsset('index.html', new sources.RawSource(index)) 74 | } 75 | } 76 | 77 | // Register the plugin in the build process, so it gets executed automatically 78 | const enhancer = (opts = {}, config) => { 79 | if (!ssr && opts.id === 'client' && !opts.isWatch) { 80 | config.plugins.push(new GenIndexPlugin()) 81 | } 82 | return config 83 | } 84 | 85 | export const webpack = enhancer 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | test 4 | 5 | 6 | test 7 | 8 | 9 |

10 |

11 | 12 | npm downloads 13 | 14 | 15 | node current 16 | 17 |

18 |

19 | 20 | node current 21 | 22 | 23 | node current 24 | 25 |

26 | 27 | # Universal Scripts 28 | 29 | Universal Scripts is a highly flexible framework for React projects, offering advanced features such as Server-Side Rendering (SSR) and internationalization (i18n). It allows you to extend or override existing configurations, giving you complete control over your setup. 30 | 31 | Additionally, its powerful plugin system provides infinite possibilities, install plugins with zero configuration or create them to fit your exact needs. Whether you want a ready-to-go setup or full customization, Universal Scripts adapts to your project seamlessly. 32 | 33 | ## Quick start 34 | 35 | If you have any maintained version of Node (at least v18) and Yarn, just run: 36 | 37 | ```bash 38 | yarn create universal-scripts my-app 39 | cd my-app 40 | yarn start 41 | ``` 42 | 43 | Then go to [http://localhost:3000](http://localhost:3000) to see your app. 44 | 45 | ## Why Use This? 46 | 47 | Universal Scripts provides a modern and flexible way to create React projects, without relying on create-react-app or react-scripts. This allows for greater control over the development and production environments. 48 | 49 | Key benefits include: 50 | 51 | - Native support for Server-Side Rendering (SSR). 52 | - Built-in internationalization (i18n). 53 | - Optimized for scalable projects. 54 | - More flexible configuration without sacrificing simplicity. 55 | - Support for custom plugins with zero config. 56 | - Typescript support. 57 | - Preconfigured Redux-Toolkit. 58 | - Advanced Data Fetching. 59 | 60 | Check out the [complete documentation](https://github.com/GlueDigital/universal-scripts/tree/master/universal-scripts) to explore examples, understand the project structure, learn how plugins work, and discover all the customization options available. 61 | -------------------------------------------------------------------------------- /universal-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-scripts", 3 | "version": "3.6.4", 4 | "description": "Build universal apps without configuration.", 5 | "type": "module", 6 | "main": "index.cjs", 7 | "module": "index.mjs", 8 | "types": "index.d.ts", 9 | "repository": "GlueDigital/universal-scripts", 10 | "author": "Glue Digital ", 11 | "homepage": "https://gluedigital.github.io/universal-scripts/", 12 | "keywords": [ 13 | "universal", 14 | "isomorphic", 15 | "react", 16 | "webpack", 17 | "ssr", 18 | "redux", 19 | "sass", 20 | "swc" 21 | ], 22 | "license": "MIT", 23 | "engines": { 24 | "node": ">=18" 25 | }, 26 | "bin": { 27 | "universal-scripts": "./bin/universal-scripts.js" 28 | }, 29 | "dependencies": { 30 | "@gluedigital/universal-plugin-helmet": "^1.0.0", 31 | "@gluedigital/universal-plugin-jest": "^1.0.0", 32 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.16", 33 | "@reduxjs/toolkit": "^2.6.1", 34 | "@swc/core": "^1.11.18", 35 | "@swc/helpers": "^0.5.15", 36 | "chalk": "^5.4.1", 37 | "cookie-parser": "^1.4.7", 38 | "copy-webpack-plugin": "^13.0.0", 39 | "cross-spawn": "^7.0.6", 40 | "css-loader": "^7.1.2", 41 | "css-minimizer-webpack-plugin": "^7.0.2", 42 | "directory-named-webpack-plugin": "^4.1.0", 43 | "dotenv": "^16.4.7", 44 | "express": "^5.1.0", 45 | "fs-extra": "^11.3.0", 46 | "js.conf.d": "2.0.1", 47 | "js.conf.d-webpack": "2.0.1", 48 | "jsesc": "^3.1.0", 49 | "mini-css-extract-plugin": "^2.9.2", 50 | "postcss": "^8.5.1", 51 | "postcss-import": "^16.1.0", 52 | "postcss-loader": "^8.1.1", 53 | "postcss-nested": "^7.0.2", 54 | "postcss-normalize": "^13.0.1", 55 | "postcss-preset-env": "^10.1.3", 56 | "postcss-url": "^10.1.3", 57 | "react-refresh": "^0.17.0", 58 | "require-from-string": "^2.0.2", 59 | "ruse-fetch": "^3.0.1", 60 | "style-loader": "^4.0.0", 61 | "swc-loader": "^0.2.6", 62 | "swc-minify-webpack-plugin": "^2.1.3", 63 | "tsconfig-paths-webpack-plugin": "^4.2.0", 64 | "typescript": "^5.8.3", 65 | "webpack": "^5.99.2", 66 | "webpack-dev-middleware": "^7.4.2", 67 | "webpack-hot-middleware": "^2.26.1", 68 | "webpack-node-externals": "^3.0.0" 69 | }, 70 | "peerDependencies": { 71 | "react": "^18 || ^19", 72 | "react-dom": "^18 || ^19", 73 | "react-intl": "^7.1.5", 74 | "react-redux": "^9.2.0", 75 | "react-router": "^7.5.0" 76 | }, 77 | "devDependencies": { 78 | "@types/express": "^5.0.1", 79 | "@types/node": "^22.14.0", 80 | "@types/react": "^19.1.0", 81 | "@types/react-dom": "^19.1.1", 82 | "@types/react-redux": "^7.1.34", 83 | "@types/webpack-env": "^1.18.8", 84 | "prettier": "3.5.3", 85 | "react-redux": "^9.2.0" 86 | }, 87 | "scripts": { 88 | "test": "bin/run-tests.sh", 89 | "lint": "eslint .", 90 | "lint:fix": "eslint . --fix", 91 | "prettier": "prettier --write ." 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /universal-scripts/runtime.conf.d/server/20-html-page.tsx: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { readFileSync } from 'node:fs' 3 | import renderHtmlLayout from '../../lib/render-html-layout' 4 | import { NextFunction, Request, Response } from 'express' 5 | 6 | const basename = process.env.SUBDIRECTORY || '/' 7 | 8 | let chunks: { name: string; size?: number }[] = [] 9 | 10 | if (!__WATCH__) { 11 | const fname = resolve('build', 'client', 'webpack-chunks.json') 12 | chunks = JSON.parse(readFileSync(fname, 'utf8')).entrypoints 13 | } 14 | 15 | let index = '' 16 | if (!__WATCH__ && !__SSR__) { 17 | const fname = resolve('build', 'client', 'index.html') 18 | index = readFileSync(fname, 'utf8') 19 | } 20 | 21 | const generateHtml = async ( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) => { 26 | if (req.originalUrl.endsWith('.json') || req.originalUrl.endsWith('.js')) { 27 | return res.status(404).end() 28 | } 29 | 30 | // Scripts and styles of the page 31 | const scripts: string[] = [] 32 | const styles: string[] = [] 33 | const reqBasename: string = basename 34 | 35 | // Add assets from build process or from client stats in watch mode 36 | let assets: string[] = [] 37 | if (!__WATCH__) { 38 | assets = chunks.map((chunk) => chunk.name) 39 | } else if (req.clientStats) { 40 | req.clientStats.entrypoints.main.assets.forEach((asset) => { 41 | if (asset.name.includes('hot-update')) return 42 | assets = assets.concat(asset.name) 43 | }) 44 | } 45 | 46 | for (const asset of assets) { 47 | if (asset.endsWith('.js')) { 48 | scripts.push(reqBasename + asset) 49 | } else if (asset.endsWith('.css')) { 50 | styles.push(``) 51 | } 52 | } 53 | 54 | // Hacer visibles nuestros recursos para otros middlewares también 55 | req.assets = { scripts, styles } 56 | res.status(200) 57 | 58 | await next() 59 | // Obtain headers from React Helmet 60 | const head = await req.triggerHook('extraHead')(req, res, { 61 | title: '', 62 | meta: '', 63 | base: '', 64 | link: '', 65 | script: '', 66 | style: '', 67 | htmlAttributes: '' 68 | }) 69 | res.write(renderHtmlLayout(head, styles)) 70 | 71 | // Send stream to client 72 | if (req.stream) { 73 | res.status(200) 74 | req.stream.pipe(res) 75 | res.end() 76 | } else { 77 | res.end() 78 | } 79 | } 80 | 81 | const envKeys = Object.keys(process.env).filter((key) => 82 | key.startsWith('PUBLIC_') 83 | ) 84 | 85 | const staticHtml = async (req: Request, res: Response, next: NextFunction) => { 86 | // Use Static HTML template 87 | await next() 88 | const envFragment = `` 89 | const newIndex = index.replace('', envFragment) 90 | res.type('text/html') 91 | res.send(newIndex) // 'index' debe ser el HTML preconstruido 92 | } 93 | 94 | export const serverMiddleware = index ? staticHtml : generateHtml 95 | -------------------------------------------------------------------------------- /universal-scripts/lib/builder.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import webpack from 'webpack' 5 | import getConfig from '../config.js' 6 | import { getUniversalConfig } from '../lib/universal-config.js' 7 | 8 | const writeAssetsToJson = (statsClient) => { 9 | const target = path.resolve( 10 | process.cwd(), 11 | 'build', 12 | 'client', 13 | 'webpack-chunks.json' 14 | ) 15 | const client = statsClient 16 | console.log( 17 | Object.values(client.entrypoints) 18 | .map((e) => e.assets) 19 | .reduce((acc, val) => acc.concat(val), []) 20 | ) 21 | const entrypoints = Object.values(client.entrypoints) 22 | .map((e) => e.assets) 23 | .reduce((acc, val) => acc.concat(val), []) // .flat() 24 | .filter((e) => !e.name.endsWith('.map')) 25 | const content = { 26 | assets: client.assets.map((a) => a.name), 27 | entrypoints 28 | } 29 | fs.writeFileSync(target, JSON.stringify(content)) 30 | console.log(chalk.green('Wrote webpack-chunks.json')) 31 | } 32 | 33 | export default async function (opts = {}) { 34 | const isWatch = !!opts.isWatch 35 | 36 | // Prepare the different build configs (client and server) 37 | const buildConfigBuilder = async (opts) => { 38 | const config = await getConfig(opts.id) 39 | return await config.webpack.reduce(async (prevPromise, enhancer) => { 40 | const cfg = await prevPromise 41 | return enhancer(opts, cfg) 42 | }, Promise.resolve({})) 43 | } 44 | 45 | const builds = ['client', 'server'] 46 | const extraBuilds = await getUniversalConfig('extraBuilds') 47 | if ( 48 | extraBuilds && 49 | Array.isArray(extraBuilds) && 50 | extraBuilds.every((b) => typeof b === 'string') 51 | ) { 52 | builds.push(...extraBuilds) 53 | } 54 | 55 | const buildConfigsPromises = builds.map((target) => 56 | buildConfigBuilder({ 57 | id: target, 58 | isWatch, 59 | ssg: !!opts.ssg 60 | }) 61 | ) 62 | 63 | const buildConfigs = await Promise.all(buildConfigsPromises) 64 | 65 | console.log(chalk.green('Build started.')) 66 | const compiler = webpack(buildConfigs) 67 | const plugin = { name: 'universal-scripts' } 68 | compiler.hooks.invalid.tap(plugin, () => { 69 | console.log('\n' + chalk.yellowBright('Compiling...')) 70 | }) 71 | compiler.hooks.done.tap(plugin, (stats) => { 72 | const statsJson = stats.toJson() 73 | if (statsJson.errors.length) { 74 | console.log(chalk.red('Failed to compile.')) 75 | } else { 76 | if (statsJson.warnings.length) { 77 | console.log(chalk.yellow('Compiled with warnings.')) 78 | } else { 79 | console.log(chalk.green('Compiled successfully.')) 80 | } 81 | if (!isWatch) { 82 | // When not using the watch mode (dev server), we need to 83 | // write the chunk info somewhere for the server to read it. 84 | writeAssetsToJson(stats.toJson().children[0]) 85 | } 86 | } 87 | console.log( 88 | stats.toString({ 89 | colors: true, 90 | chunks: false, 91 | modules: false, 92 | entrypoints: false 93 | }) 94 | ) 95 | }) 96 | return compiler 97 | } 98 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-jest/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | test 4 | 5 | 6 | test 7 | 8 | 9 |

10 |

11 | 12 | npm downloads 13 | 14 | 15 | node current 16 | 17 |

18 |

19 | 20 | node current 21 | 22 | 23 | node current 24 | 25 |

26 | 27 | # Universal Plugin Jest 28 | 29 | This is a plugin for the [`universal-scripts`](https://github.com/GlueDigital/universal-scripts/tree/master/universal-scripts) framework. It provides configuration for running tests with [`Jest`](https://jestjs.io/) in Universal-based projects. 30 | 31 | ## Features 32 | 33 | - Preconfigured setup for running tests with Jest. 34 | - Supports unit and integration testing in React applications. 35 | - Fully compatible with TypeScript and [`SWC`](https://swc.rs/) for faster builds. 36 | - Works with `@testing-library/react` and `@testing-library/jest-dom` for component testing (optional). 37 | 38 | ## Installation 39 | 40 | To install the plugin, run: 41 | 42 | ```sh 43 | yarn add @gluedigital/universal-plugin-jest 44 | ``` 45 | 46 | If using npm 47 | 48 | ```sh 49 | npm install @gluedigital/universal-plugin-jest 50 | ``` 51 | 52 | Now you can execute the tests with: 53 | 54 | ```sh 55 | yarn test 56 | ``` 57 | 58 | If using npm 59 | 60 | ```sh 61 | npm run test 62 | ``` 63 | 64 | If you need to test React components, you can install `@testing-library/react` and `@testing-library/jest-dom` for additional utilities: 65 | 66 | ```sh 67 | yarn add -D @testing-library/react @testing-library/jest-dom 68 | ``` 69 | 70 | If using npm 71 | 72 | ```sh 73 | npm install -D @testing-library/react @testing-library/jest-dom 74 | ``` 75 | 76 | Once installed, you can import them in your tests: 77 | 78 | ```typescript 79 | import { render, screen } from "@testing-library/react"; 80 | import "@testing-library/jest-dom"; 81 | import MyComponent from "./MyComponent"; 82 | 83 | test("renders the component correctly", () => { 84 | render(); 85 | expect(screen.getByText("Hello, Jest!")).toBeInTheDocument(); 86 | }); 87 | ``` 88 | 89 | ## Why Use This Plugin? 90 | 91 | - **Plug & Play**: No need for complex Jest configurations—just install and start testing. 92 | - **Performance Optimized**: Uses SWC for faster transformations. 93 | - **Flexible**: Works with both JavaScript and TypeScript projects. 94 | -------------------------------------------------------------------------------- /universal-scripts/docs/build-system.md: -------------------------------------------------------------------------------- 1 | ## The build system 2 | 3 | This section details how Webpack is configured for generating your project's build. Familiarity with Webpack configuration is assumed. 4 | 5 | ### Build config 6 | 7 | All the build config is managed with [js.conf.d](https://www.npmjs.com/package/js.conf.d). This allows us to split the config on multiple files, each dealing with different functionalities, and also makes it possible to add new configs by just creating a file on the `build.conf.d` folder of the project. You can even override the built-in config by naming your file like the piece you want to replace. 8 | 9 | To learn more about the config pieces that are included by default, you can check the `build.conf.d` folder on this project's root. 10 | 11 | You can now configure common settings for your app using the `universal.config.mjs` file at the root of your project. This file allows you to export a plugins object to customize plugin-specific options, as well as a default export for the main Universal configuration. 12 | 13 | Currently, Universal supports the following options: 14 | 15 | `noSsr`: disables server-side rendering, so the server returns an empty index that loads only the client scripts. 16 | 17 | `extraBuilds`: an array of strings specifying the names of additional builds. 18 | 19 | ### Build modes 20 | 21 | Webpack is configured in _multi build_ mode to create two builds: a _client_ bundle, and a _server_ bundle. 22 | Each of these bundles also has two modes: the _watch_ mode, used on `npm start`, and the _build_ mode, used on `npm run build` and when deployed. 23 | 24 | When in _watch_ mode, no files get written to disk, and both client and server access their bundles from memory directly, thanks to the [webpack dev middleware](https://github.com/webpack/webpack-dev-middleware). When generating a _build_ instead, each bundle goes in a different folder on the `build/` dir. 25 | 26 | Using the _multi build_ mode reduces the number of filesystem watchers needed, and should make things faster. It also makes possible for the same dev middleware to keep track of both builds, which makes server-side HMR work the same way as client-side HMR. 27 | 28 | ### Client build 29 | 30 | There are two different bundles for the client side: 31 | 32 | - `polyfills`: Loaded only on browsers which require them, before starting rendering. Can be overriden by creating `src/polyfills.js` on your project. 33 | - `main`: Deals with everything else, including initialization and rendering. Can be configured with the runtime config system. 34 | 35 | ### Server build 36 | 37 | The server build varies depending on the selected mode. When on watch mode, we run `server/main.js` with Node, and it starts the compiler with `serverMiddleware` as the entrypoint, so the server can hot-reload it from memory through the compiler. On the other hand, for the _build_ mode the server can't be the one running the compiler, as we want to compile first and run the bundle later. For this reason, this build has `server/main.js` as the entrypoint, and it gets bundled directly with the `serverMiddleware`, without the HMR code. 38 | 39 | ### Static assets 40 | 41 | The files on `src/static` are served from that folder directly during _watch_ mode, as we don't need to keep them in memory. When generating a _build_ mode bundle, they get copied directly to the build folder. 42 | 43 | When referenced from CSS, images and fonts get a suffix with a part of the file contents hash, for cache-busting. 44 | 45 | ## Next steps 46 | 47 | The build system makes it possible to run code on the client and server, but to learn more about that code, you can check the [runtime system](runtime-system). 48 | -------------------------------------------------------------------------------- /universal-scripts/build.conf.d/30-css.js: -------------------------------------------------------------------------------- 1 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 2 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin' 3 | import PostCssUrl from 'postcss-url' 4 | import postcssCascadeLayers from '@csstools/postcss-cascade-layers' 5 | import PostCssPresetEnv from 'postcss-preset-env' 6 | import PostCssNested from 'postcss-nested' 7 | import PostCssImport from 'postcss-import' 8 | import PostCssNormalize from 'postcss-normalize' 9 | import { triggerHook } from '../lib/plugins/trigger.js' 10 | 11 | const getInitialStyleConfig = (opts) => { 12 | const isServerSide = opts.id === 'server' 13 | 14 | const transformAssetUrl = (asset) => { 15 | const isRootImport = asset.url[0] === '/' && asset.url[1] !== '/' 16 | return isRootImport ? '~src/static' + asset.url : asset.url 17 | } 18 | 19 | const cssLoader = { 20 | loader: 'css-loader', 21 | options: { sourceMap: true, importLoaders: 1 } 22 | } 23 | 24 | const postcssLoader = { 25 | loader: 'postcss-loader', 26 | options: { 27 | postcssOptions: { 28 | ident: 'postcss', 29 | to: 'src/static', 30 | plugins: [ 31 | PostCssImport(), 32 | PostCssPresetEnv({ 33 | autoprefixer: { grid: true }, 34 | features: { 35 | 'nesting-rules': true 36 | } 37 | }), 38 | PostCssNested(), 39 | postcssCascadeLayers(), 40 | PostCssUrl({ url: transformAssetUrl }), 41 | PostCssNormalize() 42 | ] 43 | } 44 | } 45 | } 46 | 47 | const styleLoaderOptions = {} 48 | if (opts.css?.insert) styleLoaderOptions.insert = opts.css.insert 49 | 50 | const styleLoader = isServerSide 51 | ? undefined 52 | : { 53 | loader: 'style-loader', 54 | options: styleLoaderOptions 55 | } 56 | 57 | return [ 58 | { 59 | loaders: [styleLoader, cssLoader, postcssLoader].filter(Boolean), 60 | exts: ['css'] 61 | } 62 | ] 63 | } 64 | 65 | const enhancer = async (opts = {}, config) => { 66 | // Extraneous builds don't usually need css support 67 | if (opts.id !== 'client' && opts.id !== 'server' && !opts.css) return config 68 | 69 | // Easy access to current build config 70 | const isServerSide = opts.id === 'server' 71 | const isWatch = opts.isWatch 72 | const isProd = process.env.NODE_ENV === 'production' 73 | 74 | const initialStyleConfig = getInitialStyleConfig(opts) 75 | 76 | const stylesExtras = await triggerHook('stylesExtras')( 77 | initialStyleConfig, 78 | opts 79 | ) 80 | 81 | stylesExtras.forEach((style) => { 82 | const test = new RegExp(`\\.(${style.exts.join('|')})$`) 83 | config.module.rules.push({ 84 | test, 85 | use: style.loaders 86 | }) 87 | }) 88 | 89 | if (!isServerSide) { 90 | // Production builds get optimized CSS 91 | if (isProd) { 92 | config.optimization.minimizer.push(new CssMinimizerPlugin()) 93 | } 94 | 95 | // Non-watch builds get CSS on a separate file 96 | if (!isWatch) { 97 | config.module.rules 98 | .filter( 99 | (rule) => 100 | rule.use && rule.use.find((entry) => entry.loader === 'css-loader') 101 | ) 102 | .forEach((rule) => { 103 | rule.use = [MiniCssExtractPlugin.loader, ...rule.use.slice(1)] 104 | }) 105 | 106 | config.plugins.push( 107 | new MiniCssExtractPlugin({ 108 | filename: '[name].[contenthash].css' 109 | }) 110 | ) 111 | } 112 | } 113 | 114 | return config 115 | } 116 | 117 | export const webpack = enhancer 118 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-ssg/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | test 4 | 5 | 6 | test 7 | 8 | 9 |

10 |

11 | 12 | npm downloads 13 | 14 | 15 | node current 16 | 17 |

18 |

19 | 20 | node current 21 | 22 | 23 | node current 24 | 25 |

26 | 27 | # Universal Plugin SSG 28 | 29 | `@gluedigital/universal-plugin-ssg` is a plugin for the [`universal-scripts`](https://github.com/GlueDigital/universal-scripts/tree/master/universal-scripts) framework that enables **Static Site Generation (SSG)**. It automatically generates static HTML files for predefined routes, improving performance and SEO while reducing server load. 30 | 31 | ## Features 32 | 33 | - **Enables Static Site Generation (SSG).** 34 | - **Improves performance** by serving pre-generated static pages. 35 | - **Enhances SEO** by ensuring pages are fully rendered and crawlable by search engines. 36 | - **Reduces server load** by serving static files instead of generating pages dynamically. 37 | - **Supports dynamic route generation** by fetching data before building static pages. 38 | 39 | ## Installation 40 | 41 | To install the plugin, run: 42 | 43 | ```sh 44 | yarn add @gluedigital/universal-plugin-ssg 45 | ``` 46 | 47 | If using npm 48 | 49 | ```sh 50 | npm install @gluedigital/universal-plugin-ssg 51 | ``` 52 | 53 | ## Configuration 54 | 55 | To define which routes should be statically generated, create a file named `static-routes.mjs` inside the **`src/routes/`** directory. This file should export an asynchronous function called `getStaticRoutes`, which returns an array of routes to be generated. 56 | 57 | ### Example 58 | 59 | ```js 60 | export async function getStaticRoutes() { 61 | return ['/', '/dashboard', '/config', '/contact'] 62 | } 63 | ``` 64 | 65 | This function can fetch data from a database or an API to dynamically determine which pages need to be generated. 66 | 67 | ## Usage 68 | 69 | To generate the static pages, run the following command: 70 | 71 | ```sh 72 | yarn plugin ssg 73 | ``` 74 | 75 | or, if using npm: 76 | 77 | ```sh 78 | npm run plugin ssg 79 | ``` 80 | 81 | This command will generate a production build and create the necessary static files. 82 | 83 | ## Benefits 84 | 85 | - **Faster Load Times** – Pre-rendered static files ensure pages load instantly. 86 | - **Better SEO** – Search engines can easily index fully rendered HTML pages. 87 | - **Lower Server Costs** – Reduced reliance on server processing for rendering. 88 | - **Improved User Experience** – Faster interactions and no delays from server-side processing. 89 | - **Scalability** – Easily handle high traffic without additional server resources. 90 | 91 | ## Notes 92 | 93 | - Ensure `static-routes.mjs` is correctly set up before running the build command. 94 | - Dynamic routes requiring user input (e.g., `/profile/:id`) should be handled differently, as they cannot be pre-generated. If you want to prerender a route like this, in the file `static-routes.mjs` you should return for example a route called `/profile/1`. 95 | - If your application includes frequently changing content, consider using a hybrid approach with both static and dynamic rendering. 96 | -------------------------------------------------------------------------------- /universal-scripts/server/main.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import chalk from 'chalk' 3 | import fs from 'fs' 4 | import express from 'express' 5 | import webpackDevMiddleware from 'webpack-dev-middleware' 6 | import webpackHotMiddleware from 'webpack-hot-middleware' 7 | import path from 'path' 8 | import cookieParser from 'cookie-parser' 9 | import requireFromString from 'require-from-string' 10 | 11 | const appDirectory = fs.realpathSync(process.cwd()) 12 | const port = process.env.PORT || 3000 13 | 14 | // Group and an array of middlewares into a single one, because express doesnt support promises on next functions 15 | const groupMiddlewares = (serverMiddleware) => async (req, res, nxt) => { 16 | const copy = [...serverMiddleware] 17 | const next = (err) => { 18 | // With our custom next we dont pass err arg to express. So we need this workaround 19 | if (err) return nxt(err) 20 | const mw = copy.shift() 21 | return !mw ? null : mw(req, res, next) 22 | } 23 | await next() 24 | } 25 | 26 | // Group and an array of middlewares into a single one, because express doesnt support promises on next functions 27 | const groupErrorMiddlewares = 28 | (serverErrorMiddleware) => async (err, req, res) => { 29 | const copy = [...serverErrorMiddleware] 30 | const next = () => { 31 | const mw = copy.shift() 32 | return !mw ? null : mw(err, req, res, next) 33 | } 34 | await next() 35 | } 36 | 37 | let configureHMR 38 | let config 39 | 40 | if (__WATCH__) { 41 | // We need to hot-reload serverMiddleware, but we're the ones building it. 42 | let serverMiddleware = null 43 | let serverErrorMiddleware = null 44 | let clientStats = null 45 | 46 | const loadServerMiddlewareProxy = (req, res, next) => { 47 | if (serverMiddleware !== null && serverMiddleware.length) { 48 | return groupMiddlewares(serverMiddleware)(req, res, next) 49 | } else { 50 | console.log('Request received, but no middleware loaded yet') 51 | } 52 | } 53 | 54 | const loadServerErrorMiddlewareProxy = (err, req, res, next) => { 55 | if (serverErrorMiddleware !== null && serverErrorMiddleware.length) { 56 | return groupErrorMiddlewares(serverErrorMiddleware)(err, req, res, next) 57 | } else { 58 | console.log('Request received, but no error middleware loaded yet') 59 | } 60 | } 61 | 62 | configureHMR = (app, compiler) => { 63 | // Enable DEV middleware 64 | const devMiddleware = webpackDevMiddleware(compiler, { 65 | stats: 'summary', 66 | publicPath: '/', 67 | serverSideRender: true 68 | }) 69 | 70 | const hotMiddleware = webpackHotMiddleware(compiler.compilers[0]) 71 | 72 | compiler.compilers[0].webpackHotMiddleware = hotMiddleware 73 | 74 | app.use(devMiddleware) 75 | app.use(hotMiddleware) 76 | 77 | app.use((req, res, next) => { 78 | req.clientStats = clientStats 79 | next() 80 | }) 81 | 82 | // Add hook to compiler to reload server middleware on rebuild 83 | const mfs = devMiddleware.context.outputFileSystem 84 | const plugin = { name: 'universal-scripts' } 85 | 86 | compiler.hooks.done.tap(plugin, async () => { 87 | clientStats = devMiddleware.context.stats.toJson().children[0] 88 | const fname = path.resolve(appDirectory, 'build', 'server', 'server.js') 89 | 90 | try { 91 | const newMiddleware = mfs.readFileSync(fname).toString() 92 | const mw = requireFromString(newMiddleware, fname) 93 | config = mw.rawConfig 94 | 95 | if (config.app) config.app.forEach((f) => f(app)) 96 | await mw.startup() 97 | serverMiddleware = mw.default 98 | serverErrorMiddleware = mw.rawConfig.serverErrorMiddleware 99 | } catch (e) { 100 | console.warn(chalk.red.bold("Couldn't load middleware.")) 101 | console.log( 102 | chalk.red( 103 | 'Please fix any build errors above, and ' + 'it will auto-reload.' 104 | ) 105 | ) 106 | console.log('Details:', e) 107 | } 108 | }) 109 | 110 | app.use(loadServerMiddlewareProxy) 111 | app.use(loadServerErrorMiddlewareProxy) 112 | } 113 | } 114 | 115 | const serve = async (compiler) => { 116 | console.log(chalk.green('Starting server.')) 117 | const app = express() 118 | 119 | app.use(cookieParser()) 120 | app.disable('x-powered-by') 121 | app.use(express.json()) 122 | 123 | if (__WATCH__) { 124 | // Add the HMR and Dev Server middleware 125 | await configureHMR(app, compiler) 126 | } else { 127 | const mw = await import('./serverMiddleware.js') 128 | 129 | config = mw.rawConfig 130 | 131 | // Run anything on the `app` hook 132 | if (config.app) config.app.forEach((f) => f(app)) 133 | // Add the server-side rendering middleware (no HMR) 134 | await mw.startup() 135 | app.use(groupMiddlewares(mw.default)) 136 | app.use(groupErrorMiddlewares(mw.rawConfig.serverErrorMiddleware)) 137 | } 138 | 139 | // Wrap it up 140 | const server = app.listen(port) 141 | 142 | // Run anything on the `app` hook 143 | if (config && config.appAfter) config.appAfter.forEach((f) => f(server)) 144 | 145 | console.log( 146 | chalk.green('Server running at:'), 147 | chalk.cyan.bold('http://localhost:' + port) 148 | ) 149 | } 150 | 151 | if (!__WATCH__) { 152 | // On static build, this is the entry point, so for it to actually run, 153 | // we must call the exported function 154 | serve() 155 | } 156 | 157 | export default serve 158 | -------------------------------------------------------------------------------- /universal-scripts/build.conf.d/05-base.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { resolve, dirname, join } from 'path' 3 | import JsconfdPlugin from 'js.conf.d-webpack' 4 | import DirectoryNamedWebpackPlugin from 'directory-named-webpack-plugin' 5 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin' 6 | import { fileURLToPath } from 'url' 7 | import { SwcMinifyWebpackPlugin } from 'swc-minify-webpack-plugin' 8 | import { 9 | findUniversalPlugins, 10 | filterPluginsWithSubdir 11 | } from '../lib/find-scripts.js' 12 | import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin' 13 | import { triggerHook } from '../lib/plugins/trigger.js' 14 | import { EnvReloadPlugin } from '../lib/vars/EnvPlugin.js' 15 | 16 | const __filename = fileURLToPath(import.meta.url) 17 | const __dirname = dirname(__filename) 18 | 19 | const appDirectory = fs.realpathSync(process.cwd()) 20 | 21 | const plugins = findUniversalPlugins() 22 | const pluginsRuntime = filterPluginsWithSubdir(plugins, 'runtime.conf.d').map( 23 | (plugin) => join(plugin, 'runtime.conf.d') 24 | ) 25 | 26 | const enhancer = async (opts = {}) => { 27 | const id = opts.id 28 | const isClientSide = id === 'client' 29 | const isProd = process.env.NODE_ENV === 'production' 30 | 31 | const pluginsRuntimeId = plugins.map((plugin) => 32 | join(plugin, 'runtime.conf.d', id) 33 | ) 34 | 35 | const initialWebpackPlugins = [ 36 | // new ProgressPlugin({}), 37 | new TsconfigPathsPlugin({ 38 | silent: true, 39 | configFile: 'tsconfig.json' 40 | }), 41 | new JsconfdPlugin({ 42 | folders: [ 43 | resolve(__dirname, '..', 'runtime.conf.d'), 44 | resolve(__dirname, '..', 'runtime.conf.d', id), 45 | resolve(appDirectory, 'runtime.conf.d'), 46 | resolve(appDirectory, 'runtime.conf.d', id), 47 | ...pluginsRuntime, 48 | ...pluginsRuntimeId 49 | ], 50 | merge: (current, add) => { 51 | for (const key of Object.keys(add)) { 52 | current[key] = current[key] || [] 53 | current[key].push(add[key]) 54 | } 55 | return current 56 | } 57 | }), 58 | !isProd && 59 | isClientSide && 60 | new ReactRefreshWebpackPlugin({ 61 | overlay: false 62 | }), 63 | isClientSide && !isProd && new EnvReloadPlugin() 64 | ] 65 | 66 | const allPlugins = await triggerHook('extraPlugins')( 67 | initialWebpackPlugins, 68 | opts 69 | ) 70 | 71 | const config = { 72 | name: id, 73 | devtool: isProd ? 'source-map' : 'cheap-module-source-map', 74 | target: isClientSide ? 'web' : 'node', 75 | mode: isProd ? 'production' : 'development', 76 | performance: { hints: false }, 77 | output: { 78 | path: resolve(appDirectory, 'build', id), 79 | pathinfo: true, 80 | filename: isClientSide ? '[name].[contenthash].js' : '[name].js', 81 | chunkFilename: isClientSide ? '[name].[contenthash].js' : '[name].js', 82 | publicPath: process.env.SUBDIRECTORY || '/', 83 | clean: true 84 | }, 85 | resolve: { 86 | extensions: [ 87 | '.wasm', 88 | '.mjs', 89 | '.ts', 90 | '.js', 91 | '.tsx', 92 | '.jsx', 93 | '.json', 94 | '.sass', 95 | '.scss', 96 | '.css' 97 | ], 98 | modules: [ 99 | resolve(__dirname, '..', 'node_modules'), 100 | resolve(appDirectory, 'node_modules'), 101 | appDirectory 102 | ], 103 | plugins: [ 104 | new DirectoryNamedWebpackPlugin({ 105 | honorIndex: true, 106 | exclude: /node_modules/ 107 | }) 108 | ], 109 | alias: { 110 | '@components': resolve(process.cwd(), 'src/components'), 111 | '@utils': resolve(process.cwd(), 'src/utils'), 112 | '@routes': resolve(process.cwd(), 'src/routes'), 113 | '@static': resolve(process.cwd(), 'src/static'), 114 | '@hooks': resolve(process.cwd(), 'src/hooks'), 115 | src: resolve(process.cwd(), 'src') 116 | } 117 | }, 118 | plugins: allPlugins.filter(Boolean), 119 | module: { 120 | rules: [ 121 | { 122 | test: /\.(js|jsx|ts|tsx|mjs)$/, 123 | exclude: 124 | /node_modules\/(?!universal-scripts|@[^/]+\/universal-plugin[^/]+).*/, 125 | loader: 'swc-loader', 126 | options: { 127 | jsc: { 128 | preserveAllComments: true, // Needed for webpack anotations 129 | parser: { 130 | syntax: 'typescript', 131 | jsx: true, 132 | tsx: true, 133 | dynamicImport: true, 134 | topLevelAwait: true 135 | }, 136 | transform: { 137 | react: { 138 | runtime: 'automatic', // Equivalent to '@babel/preset-react' 139 | refresh: !isProd && isClientSide // Equivalent to React Refresh 140 | } 141 | }, 142 | target: 'es2022', // Similar to @babel/preset-env 143 | externalHelpers: true // Equivalent to '@babel/plugin-transform-runtime' 144 | } 145 | } 146 | }, 147 | { 148 | test: /\.(jpg|png|gif|webp|svg|ico|avif|mp4|webm)$/i, 149 | type: 'asset/resource', 150 | generator: { 151 | filename: 'static/[name].[contenthash][ext]' 152 | } 153 | }, 154 | { 155 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 156 | type: 'asset/resource', 157 | generator: { 158 | filename: 'static/[name].[contenthash][ext]' 159 | } 160 | } 161 | ] 162 | }, 163 | optimization: { 164 | minimize: isProd, 165 | minimizer: [new SwcMinifyWebpackPlugin()], 166 | splitChunks: { 167 | cacheGroups: { 168 | vendor: { 169 | test: /[\\/]node_modules[\\/]/, 170 | name: 'vendors' 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | return config 178 | } 179 | 180 | export const webpack = enhancer 181 | -------------------------------------------------------------------------------- /universal-scripts/README.md: -------------------------------------------------------------------------------- 1 |

2 | test 3 | test 4 |

5 |

6 | npm downloads 7 | node current 8 |

9 |

10 | node current 11 | node current 12 |

13 | 14 | # Universal Scripts 15 | 16 | Universal Scripts is a highly flexible framework for React projects, offering advanced features such as Server-Side Rendering (SSR) and internationalization (i18n). It allows you to extend or override existing configuration easily, giving you complete control over your setup. 17 | 18 | - Server-Side Rendering. 19 | - TypeScript support including custom aliases. 20 | - Internationalization with [`react-intl`](https://github.com/ericf/react-intl), 21 | - Metadata management with [`react-helmet-async`](https://github.com/staylor/react-helmet-async). 22 | - Redux state management with [`redux-toolkit`](https://github.com/reduxjs/redux-toolkit) and types support. 23 | - Integrated with [`ruse-fetch`](https://github.com/GlueDigital/ruse-fetch) to provide a modern way of fetching data with `Suspense`. 24 | - Use [`SWC`](https://github.com/swc-project/swc) for better performance. 25 | - Hot Reload in Server and Client, including .env file. 26 | 27 | ## Project Structure 28 | 29 | You can use the pre-built templates, such as the TypeScript template, or create your own to match your preferences. Below are the main folders defined in the default template: 30 | 31 | - `src/locales`: Store your translations, the first key in `index.ts` is the default language. 32 | - `src/routes`: The index file serves as the root component, where you can define your application routes with `react-router`. 33 | - `src/static`: Contains static assets like images, fonts, etc. These files will be copied to the build. 34 | - `src/store`: Add your slices and place them inside the slices folder. 35 | - `src/styles`: The main entry point for global styles. 36 | 37 | These are the default folders, but you can create additional ones such as components, hooks, contexts, and more. Additionally, the tsconfig file includes predefined aliases, which you can customize or extend as needed. 38 | 39 | ## Plugins 40 | 41 | In Universal, you have the flexibility to use pre-built plugins or develop your own. These plugins are designed to work seamlessly without requiring any additional configuration—just install them, and they are ready to use. This allows for a more efficient development process, enabling you to extend functionality effortlessly while maintaining a clean and modular project structure. 42 | 43 | This documentation describes the configuration of the following universal pre-installed plugins in a project: 44 | 45 | ### [`universal-plugin-helmet`](https://github.com/GlueDigital/universal-scripts/tree/master/universal-plugins/universal-plugin-helmet) 46 | 47 | This plugin introduces configuration for [`react-helmet-async`](https://github.com/staylor/react-helmet-async), enabling efficient metadata management in React applications. 48 | 49 | #### Features: 50 | 51 | - Enables full functionality of react-helmet-async. 52 | - Allows dynamic management in a React application. 53 | - Improves SEO optimization and accessibility. 54 | - Enables customizable social sharing with dynamic Open Graph metadata. 55 | 56 | ### [`universal-plugin-jest`](https://github.com/GlueDigital/universal-scripts/tree/master/universal-plugins/universal-plugin-jest) 57 | 58 | This plugin configures [`Jest`](https://github.com/jestjs/jest), to run your test suites. 59 | 60 | #### Features: 61 | 62 | - Configures Jest for unit and integration testing. 63 | - Use SWC for better performance. 64 | 65 | ### Custom Plugins 66 | 67 | In addition to the pre-installed plugins, you can create your own plugins or use other existing ones, such as the [`universal-plugin-sass`](https://github.com/GlueDigital/universal-scripts/tree/master/universal-plugins/universal-plugin-sass) for Sass support. 68 | 69 | If you want to use for example [`universal-plugin-sass`](https://github.com/GlueDigital/universal-scripts/tree/master/universal-plugins/universal-plugin-sass) you just have to install this as a dependency. And universal will recognize them without any configuration. 70 | 71 | ```bash 72 | yarn add universal-plugin-sass 73 | ``` 74 | 75 | If using npm 76 | 77 | ```sh 78 | npm install universal-plugin-sass 79 | ``` 80 | 81 | ## Data Fetching 82 | 83 | Universal is already configured to use [`ruse-fetch`](https://github.com/GlueDigital/ruse-fetch), making data fetching simple and efficient. 84 | 85 | ```typescript 86 | import { useFetch } from 'ruse-fetch' 87 | 88 | const Users = () => { 89 | const users = useFetch('https://reqres.in/api/users') 90 | return ( 91 |
    92 | {users.data.map((u) => ( 93 |
  • u.first_name
  • 94 | ))} 95 |
96 | ) 97 | } 98 | 99 | const Home = () => { 100 | return ( 101 |
102 | ... 103 | Loading...}> 104 | 105 | 106 | ... 107 |
108 | ) 109 | } 110 | 111 | ``` 112 | 113 | ## Integration with Redux Toolkit 114 | 115 | To maintain a structured and scalable Redux store in your application, follow this setup. 116 | 117 | ### Folder Structure 118 | 119 | Inside the store directory, use the slices folder where all your Redux slices will be stored. Then, import all slices into the central reducers file. 120 | 121 | This ensures that Universal will automatically recognize all slice types and include them in the store, providing full type safety. 122 | 123 | ### Using useAppSelector 124 | 125 | With `useAppSelector`, you can access the fully-typed Redux store, including elements provided by Universal. 126 | 127 | #### Example: Language Selector Component 128 | 129 | ```typescript 130 | 131 | import { updateIntl, useAppDispatch, useAppSelector } from 'universal-scripts' 132 | import locales from 'src/locales' 133 | 134 | function SelectLanguage() { 135 | const locale = useAppSelector((state) => state.intl.lang) 136 | const dispatch = useAppDispatch() 137 | 138 | const changeLang = (event) => { 139 | const lang = event.target.value 140 | dispatch( 141 | updateIntl({ 142 | lang, 143 | messages: locales[lang] 144 | }) 145 | ) 146 | } 147 | 148 | return ( 149 | 156 | ) 157 | } 158 | ``` 159 | 160 | ## Enviroment Variables 161 | 162 | Environment variables are declared in the .env file. These variables are not included in the generated build by default, ensuring that sensitive information is not stored in the build. The variables are read during the application startup and are sent from the server to the client. Only variables that start with `PUBLIC_` are passed from the server to the client. If server-side rendering (SSR) is disabled, the variables are still sent to the client in the same way. 163 | 164 | If you modify the `.env` file in development, Universal will automatically perform a hot reload with the updated variable values. In production mode, you only need to restart the app to apply the new variables—there’s no need to rebuild the app to see the changes. 165 | 166 | ## Inner Structure 167 | 168 | This section explains how the main folders work in Universal. The core is built around [`js.conf.d`](https://github.com/mancontr/js.conf.d), which allows us to split the configuration into multiple files. This approach makes it possible to create new configurations or even override the built-in ones. 169 | 170 | - **`build.conf.d`** – Contains everything related to the Webpack bundling process. 171 | - **`runtime.conf.d`** – Manages configurations related to the runtime of the application, such as redux, render.... 172 | - **`lib`** – Provides common functionality and utilities. 173 | - **`scripts`** – Contains scripts defined in the `scripts` section of `package.json`, used for automation and task execution. 174 | - **`server`** – The main entry point for the server, containing all configurations for Express and middleware setup. 175 | - **`client`** – The main entry point for the client-side application. 176 | 177 | With this structure configurations in this way, Universal enables modular, maintainable, and customizable setups. 🚀 178 | 179 | Check out the [documentation](https://gluedigital.github.io/universal-scripts) to explore all features or follow the [getting started](https://gluedigital.github.io/universal-scripts/getting-started) guide. 180 | 181 | ## Configuration 182 | 183 | For common use cases, support has been added to define configuration in a `universal.config.mjs` file located at the root of your application. 184 | 185 | You can export a `plugins` object to customize specific plugin options, and a `default` export for the main Universal configuration. 186 | 187 | Currently, the following options are supported: 188 | 189 | - `noSsr`: Disables server-side rendering. The server will return a minimal HTML file that only loads the client scripts. 190 | 191 | - `extraBuilds`: An array of strings representing the names of additional builds to include. 192 | -------------------------------------------------------------------------------- /universal-plugins/universal-plugin-sass/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@parcel/watcher-android-arm64@2.5.1": 6 | version "2.5.1" 7 | resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" 8 | integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== 9 | 10 | "@parcel/watcher-darwin-arm64@2.5.1": 11 | version "2.5.1" 12 | resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" 13 | integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== 14 | 15 | "@parcel/watcher-darwin-x64@2.5.1": 16 | version "2.5.1" 17 | resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" 18 | integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== 19 | 20 | "@parcel/watcher-freebsd-x64@2.5.1": 21 | version "2.5.1" 22 | resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" 23 | integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== 24 | 25 | "@parcel/watcher-linux-arm-glibc@2.5.1": 26 | version "2.5.1" 27 | resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" 28 | integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== 29 | 30 | "@parcel/watcher-linux-arm-musl@2.5.1": 31 | version "2.5.1" 32 | resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" 33 | integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== 34 | 35 | "@parcel/watcher-linux-arm64-glibc@2.5.1": 36 | version "2.5.1" 37 | resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" 38 | integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== 39 | 40 | "@parcel/watcher-linux-arm64-musl@2.5.1": 41 | version "2.5.1" 42 | resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" 43 | integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== 44 | 45 | "@parcel/watcher-linux-x64-glibc@2.5.1": 46 | version "2.5.1" 47 | resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" 48 | integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== 49 | 50 | "@parcel/watcher-linux-x64-musl@2.5.1": 51 | version "2.5.1" 52 | resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" 53 | integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== 54 | 55 | "@parcel/watcher-win32-arm64@2.5.1": 56 | version "2.5.1" 57 | resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" 58 | integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== 59 | 60 | "@parcel/watcher-win32-ia32@2.5.1": 61 | version "2.5.1" 62 | resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" 63 | integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== 64 | 65 | "@parcel/watcher-win32-x64@2.5.1": 66 | version "2.5.1" 67 | resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" 68 | integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== 69 | 70 | "@parcel/watcher@^2.4.1": 71 | version "2.5.1" 72 | resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200" 73 | integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== 74 | dependencies: 75 | detect-libc "^1.0.3" 76 | is-glob "^4.0.3" 77 | micromatch "^4.0.5" 78 | node-addon-api "^7.0.0" 79 | optionalDependencies: 80 | "@parcel/watcher-android-arm64" "2.5.1" 81 | "@parcel/watcher-darwin-arm64" "2.5.1" 82 | "@parcel/watcher-darwin-x64" "2.5.1" 83 | "@parcel/watcher-freebsd-x64" "2.5.1" 84 | "@parcel/watcher-linux-arm-glibc" "2.5.1" 85 | "@parcel/watcher-linux-arm-musl" "2.5.1" 86 | "@parcel/watcher-linux-arm64-glibc" "2.5.1" 87 | "@parcel/watcher-linux-arm64-musl" "2.5.1" 88 | "@parcel/watcher-linux-x64-glibc" "2.5.1" 89 | "@parcel/watcher-linux-x64-musl" "2.5.1" 90 | "@parcel/watcher-win32-arm64" "2.5.1" 91 | "@parcel/watcher-win32-ia32" "2.5.1" 92 | "@parcel/watcher-win32-x64" "2.5.1" 93 | 94 | braces@^3.0.3: 95 | version "3.0.3" 96 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" 97 | integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== 98 | dependencies: 99 | fill-range "^7.1.1" 100 | 101 | chokidar@^4.0.0: 102 | version "4.0.3" 103 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" 104 | integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== 105 | dependencies: 106 | readdirp "^4.0.1" 107 | 108 | css-loader@^7.1.2: 109 | version "7.1.2" 110 | resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.2.tgz#64671541c6efe06b0e22e750503106bdd86880f8" 111 | integrity sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA== 112 | dependencies: 113 | icss-utils "^5.1.0" 114 | postcss "^8.4.33" 115 | postcss-modules-extract-imports "^3.1.0" 116 | postcss-modules-local-by-default "^4.0.5" 117 | postcss-modules-scope "^3.2.0" 118 | postcss-modules-values "^4.0.0" 119 | postcss-value-parser "^4.2.0" 120 | semver "^7.5.4" 121 | 122 | cssesc@^3.0.0: 123 | version "3.0.0" 124 | resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" 125 | integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== 126 | 127 | detect-libc@^1.0.3: 128 | version "1.0.3" 129 | resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" 130 | integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== 131 | 132 | fill-range@^7.1.1: 133 | version "7.1.1" 134 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" 135 | integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== 136 | dependencies: 137 | to-regex-range "^5.0.1" 138 | 139 | icss-utils@^5.0.0, icss-utils@^5.1.0: 140 | version "5.1.0" 141 | resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" 142 | integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== 143 | 144 | immutable@^5.0.2: 145 | version "5.0.3" 146 | resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1" 147 | integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== 148 | 149 | is-extglob@^2.1.1: 150 | version "2.1.1" 151 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 152 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 153 | 154 | is-glob@^4.0.3: 155 | version "4.0.3" 156 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 157 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 158 | dependencies: 159 | is-extglob "^2.1.1" 160 | 161 | is-number@^7.0.0: 162 | version "7.0.0" 163 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 164 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 165 | 166 | micromatch@^4.0.5: 167 | version "4.0.8" 168 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" 169 | integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== 170 | dependencies: 171 | braces "^3.0.3" 172 | picomatch "^2.3.1" 173 | 174 | nanoid@^3.3.8: 175 | version "3.3.8" 176 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" 177 | integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== 178 | 179 | neo-async@^2.6.2: 180 | version "2.6.2" 181 | resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" 182 | integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== 183 | 184 | node-addon-api@^7.0.0: 185 | version "7.1.1" 186 | resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" 187 | integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== 188 | 189 | picocolors@^1.1.1: 190 | version "1.1.1" 191 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" 192 | integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== 193 | 194 | picomatch@^2.3.1: 195 | version "2.3.1" 196 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 197 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 198 | 199 | postcss-modules-extract-imports@^3.1.0: 200 | version "3.1.0" 201 | resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" 202 | integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== 203 | 204 | postcss-modules-local-by-default@^4.0.5: 205 | version "4.2.0" 206 | resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" 207 | integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== 208 | dependencies: 209 | icss-utils "^5.0.0" 210 | postcss-selector-parser "^7.0.0" 211 | postcss-value-parser "^4.1.0" 212 | 213 | postcss-modules-scope@^3.2.0: 214 | version "3.2.1" 215 | resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" 216 | integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== 217 | dependencies: 218 | postcss-selector-parser "^7.0.0" 219 | 220 | postcss-modules-values@^4.0.0: 221 | version "4.0.0" 222 | resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" 223 | integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== 224 | dependencies: 225 | icss-utils "^5.0.0" 226 | 227 | postcss-selector-parser@^7.0.0: 228 | version "7.1.0" 229 | resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262" 230 | integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA== 231 | dependencies: 232 | cssesc "^3.0.0" 233 | util-deprecate "^1.0.2" 234 | 235 | postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: 236 | version "4.2.0" 237 | resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" 238 | integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== 239 | 240 | postcss@^8.4.33: 241 | version "8.5.2" 242 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.2.tgz#e7b99cb9d2ec3e8dd424002e7c16517cb2b846bd" 243 | integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA== 244 | dependencies: 245 | nanoid "^3.3.8" 246 | picocolors "^1.1.1" 247 | source-map-js "^1.2.1" 248 | 249 | readdirp@^4.0.1: 250 | version "4.1.1" 251 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.1.tgz#bd115327129672dc47f87408f05df9bd9ca3ef55" 252 | integrity sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw== 253 | 254 | sass-loader@^16.0.4: 255 | version "16.0.4" 256 | resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-16.0.4.tgz#5c2afb755fbc0a45a004369efa11579518a39a45" 257 | integrity sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg== 258 | dependencies: 259 | neo-async "^2.6.2" 260 | 261 | sass@^1.84.0: 262 | version "1.84.0" 263 | resolved "https://registry.yarnpkg.com/sass/-/sass-1.84.0.tgz#da9154cbccb2d2eac7a9486091b6d9ba93ef5bad" 264 | integrity sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg== 265 | dependencies: 266 | chokidar "^4.0.0" 267 | immutable "^5.0.2" 268 | source-map-js ">=0.6.2 <2.0.0" 269 | optionalDependencies: 270 | "@parcel/watcher" "^2.4.1" 271 | 272 | semver@^7.5.4: 273 | version "7.7.1" 274 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" 275 | integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== 276 | 277 | "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: 278 | version "1.2.1" 279 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" 280 | integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== 281 | 282 | to-regex-range@^5.0.1: 283 | version "5.0.1" 284 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 285 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 286 | dependencies: 287 | is-number "^7.0.0" 288 | 289 | util-deprecate@^1.0.2: 290 | version "1.0.2" 291 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 292 | integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== 293 | --------------------------------------------------------------------------------