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 |
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 |
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 |
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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
--------------------------------------------------------------------------------