@asd14/vblocks 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/vblocks.sh CHANGED
@@ -8,20 +8,23 @@
8
8
  #
9
9
  # Available Commands:
10
10
  # ---
11
+ # init: Creates a new project from the template
11
12
  # build: Creates a production-ready build of your project
12
13
  # serve: Starts a development server with live reload
13
- # eject: Copies default webpack and swc configs to your project for customization
14
+ # eject: Prints a default config, save it in your project to customize it
14
15
  #
15
16
  # Usage Examples:
16
17
  # ---
18
+ # npx vblocks init my-app # Create a project in ./my-app
19
+ # npx vblocks init my-flow --template xyflow-playground
17
20
  # npx vblocks build # Build for production
18
21
  # npx vblocks serve # Start dev server
19
- # npx vblocks eject # Copy config files to your project
22
+ # npx vblocks eject webpack > webpack.config.js # Customize a config
20
23
  # =============================================================================
21
24
 
22
25
  # Directory where this script is located and
23
26
  # default configuration file paths (from this package).
24
- export VB_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
27
+ export VB_DIR="$(dirname "$(dirname "$(realpath "$0")")")"
25
28
 
26
29
  # Project directory (where you run the command from) and
27
30
  # project-specific config file paths.
@@ -37,7 +40,6 @@ select_config() {
37
40
  # Determine config files location.
38
41
  export VB_WEBPACK_CONFIG="$(select_config "webpack.config.js")"
39
42
  export VB_POSTCSS_CONFIG="$(select_config "postcss.config.js")"
40
- export VB_TAILWIND_CONFIG="$(select_config "tailwind.config.js")"
41
43
  export VB_SWC_CONFIG="$(select_config ".swcrc")"
42
44
 
43
45
  # Wrap webpack command with custom configs
@@ -62,15 +64,90 @@ run_webpack() {
62
64
  "$@"
63
65
  }
64
66
 
65
- # Copy config files to your project
66
- copy_config_files() {
67
- project_path="$PROJECT_DIR/$1"
68
- default_path=$(realpath "$VB_DIR/$1")
67
+ # Print the template names, one per line. A template is a folder in
68
+ # `template/` whose name has no `_` prefix
69
+ list_templates() {
70
+ for list_dir in "$VB_DIR"/template/*/; do
71
+ [ -d "$list_dir" ] || continue
72
+ list_name=$(basename "$list_dir")
73
+
74
+ case "$list_name" in
75
+ _*) ;;
76
+ *) printf '%s\n' "$list_name" ;;
77
+ esac
78
+ done
79
+ }
80
+
81
+ # Copy `template/_common`, then one template over it, into a directory that
82
+ # is missing or empty
83
+ init_project() {
84
+ init_usage="Usage: vblocks init <directory> [--template <name>]"
85
+ init_dir=""
86
+ init_template="plain"
87
+
88
+ while [ "$#" -gt 0 ]; do
89
+ case "$1" in
90
+ --template)
91
+ if [ "$#" -lt 2 ]; then
92
+ echo "$init_usage" >&2
93
+ exit 1
94
+ fi
95
+ init_template=$2
96
+ shift 2
97
+ ;;
98
+ -*)
99
+ echo "$init_usage" >&2
100
+ exit 1
101
+ ;;
102
+ *)
103
+ if [ -n "$init_dir" ]; then
104
+ echo "$init_usage" >&2
105
+ exit 1
106
+ fi
107
+ init_dir=$1
108
+ shift
109
+ ;;
110
+ esac
111
+ done
69
112
 
70
- if [ ! -f "$project_path" ]; then
71
- cp "$default_path" "$project_path" \
72
- && echo "Created $1 in your project" >&2
113
+ if [ -z "$init_dir" ]; then
114
+ echo "$init_usage" >&2
115
+ exit 1
73
116
  fi
117
+
118
+ init_templates=$(list_templates)
119
+ if ! printf '%s\n' "$init_templates" | grep -qxF -- "$init_template"; then
120
+ printf 'Unknown template "%s". Available: %s\n' "$init_template" \
121
+ "$(printf '%s\n' "$init_templates" | paste -sd, - | sed 's/,/, /g')" >&2
122
+ exit 1
123
+ fi
124
+
125
+ if [ -e "$init_dir" ] && [ -n "$(ls -A -- "$init_dir")" ]; then
126
+ printf '%s exists and is not empty\n' "$init_dir" >&2
127
+ exit 1
128
+ fi
129
+
130
+ mkdir -p -- "$init_dir" || exit 1
131
+ cp -R -- "$VB_DIR/template/_common/." "$init_dir" || exit 1
132
+ cp -R -- "$VB_DIR/template/$init_template/." "$init_dir" || exit 1
133
+
134
+ # npm leaves every `.gitignore` out of the package, so the template ships
135
+ # `_gitignore`
136
+ mv -- "$init_dir/_gitignore" "$init_dir/.gitignore" || exit 1
137
+
138
+ init_name=$(basename "$(realpath "$init_dir")")
139
+ init_version=$(node -p "require('$VB_DIR/package.json').version")
140
+
141
+ (
142
+ cd -- "$init_dir" \
143
+ && npm pkg set "name=$init_name" \
144
+ "devDependencies.@asd14/vblocks=^$init_version"
145
+ ) || exit 1
146
+
147
+ printf 'Created %s. Next:\n\n' "$init_name"
148
+ printf ' cd %s\n' "$init_dir"
149
+ printf ' npm install\n'
150
+ printf ' npm start\n'
74
151
  }
75
152
 
76
153
  print_hero() {
@@ -103,7 +180,6 @@ print_hero() {
103
180
  printf "│\n"
104
181
  printf "│ %b\n" " Webpack: $(format_config_path "$VB_WEBPACK_CONFIG")"
105
182
  printf "│ %b\n" " PostCSS: $(format_config_path "$VB_POSTCSS_CONFIG")"
106
- printf "│ %b\n" " Tailwind: $(format_config_path "$VB_TAILWIND_CONFIG")"
107
183
  printf "│ %b\n" "󱡮 SWC: $(format_config_path "$VB_SWC_CONFIG")"
108
184
  printf "╰──┤\n\n"
109
185
  }
@@ -112,16 +188,23 @@ print_hero() {
112
188
  # │ Main
113
189
  # ╰────────
114
190
 
115
- print_hero
191
+ # stdout carries the config `eject` prints, keep the hero out of it
192
+ print_hero >&2
116
193
 
117
194
  case "$1" in
195
+ init)
196
+ shift
197
+ init_project "$@"
198
+ ;;
118
199
  build)
119
200
  shift
120
- run_webpack build "$@"
201
+ # Without a mode Webpack falls back to `production`, also for `serve`.
202
+ # A `--mode` in "$@" comes later, so it wins.
203
+ run_webpack build --mode production "$@"
121
204
  ;;
122
205
  serve)
123
206
  shift
124
- run_webpack serve "$@"
207
+ run_webpack serve --mode development "$@"
125
208
  ;;
126
209
  eject)
127
210
  shift
@@ -129,16 +212,22 @@ case "$1" in
129
212
 
130
213
  case "$file" in
131
214
  webpack)
132
- cat "$VB_DIR/webpack.config.js"
215
+ cat "$VB_DIR/src/webpack.config.js"
216
+ ;;
217
+ postcss)
218
+ cat "$VB_DIR/src/postcss.config.js"
219
+ ;;
220
+ swc)
221
+ cat "$VB_DIR/src/.swcrc"
133
222
  ;;
134
223
  *)
135
- echo "Unknown config file to eject. Available: webpack, postcss, tailwind and swc" >&2
224
+ echo "Unknown config file to eject. Available: webpack, postcss and swc" >&2
136
225
  exit 1
137
226
  ;;
138
227
  esac
139
228
  ;;
140
229
  *)
141
- echo "Usage: $0 {build|serve|eject}" >&2
230
+ echo "Usage: $0 {init|build|serve|eject}" >&2
142
231
  exit 1
143
232
  ;;
144
233
  esac
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@asd14/vblocks",
4
- "version": "1.0.0",
4
+ "version": "2.0.0",
5
5
  "description": "󱆃 POSIX +  Webpack, an unlikely story!",
6
+ "license": "MIT",
6
7
  "sideEffects": false,
7
8
  "publishConfig": {
8
9
  "access": "public"
@@ -19,9 +20,16 @@
19
20
  },
20
21
  "files": [
21
22
  "bin",
22
- "src"
23
+ "src",
24
+ "template"
23
25
  ],
24
26
  "type": "module",
27
+ "exports": {
28
+ "./client": {
29
+ "types": "./src/types/client.d.ts"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
25
33
  "bin": {
26
34
  "vblocks": "bin/vblocks.sh"
27
35
  },
@@ -29,53 +37,55 @@
29
37
  "----UTIL": "",
30
38
  "prepare": "git config --local core.hooksPath .githook",
31
39
  "format": "eslint --fix .",
32
- "----CODE-ANALYSIS": "",
40
+ "----CODE-QA": "",
33
41
  "lint": "eslint .",
34
- "typecheck": "tsc --noEmit"
42
+ "typecheck": "tsc --noEmit",
43
+ "unit-test:template": "tools/verify-templates",
44
+ "unit-test": "npm run unit-test:template"
35
45
  },
36
46
  "dependencies": {
37
- "@csstools/postcss-global-data": "^3.1.0",
38
47
  "@mdx-js/loader": "^3.1.1",
39
- "@pmmmwh/react-refresh-webpack-plugin": "^0.6.1",
40
- "browserslist": "^4.26.2",
41
- "copy-webpack-plugin": "^13.0.1",
42
- "css-loader": "^7.1.2",
43
- "cssnano": "^7.1.1",
44
- "dotenv-webpack": "^8.1.1",
45
- "html-webpack-plugin": "^5.6.4",
46
- "mini-css-extract-plugin": "^2.9.4",
47
- "postcss": "^8.5.6",
48
- "postcss-loader": "^8.2.0",
49
- "postcss-preset-env": "^10.3.1",
50
- "react-refresh": "^0.17.0",
51
- "style-loader": "^4.0.0",
52
- "swc-loader": "^0.2.6",
53
- "tailwindcss": "^4.1.13",
54
- "webpack": "^5.101.3",
55
- "webpack-bundle-analyzer": "^4.10.2",
56
- "webpack-cli": "^6.0.1",
57
- "webpack-dev-server": "^5.2.2"
48
+ "@pmmmwh/react-refresh-webpack-plugin": "^0.6.3",
49
+ "@swc/helpers": "^0.5.23",
50
+ "browserslist": "^4.29.3",
51
+ "copy-webpack-plugin": "^14.0.0",
52
+ "css-loader": "^7.1.5",
53
+ "cssnano": "^9.6.0",
54
+ "dotenv-webpack": "^9.0.0",
55
+ "html-webpack-plugin": "^5.6.8",
56
+ "mini-css-extract-plugin": "^2.10.2",
57
+ "postcss": "^8.5.29",
58
+ "postcss-loader": "^8.2.1",
59
+ "postcss-preset-env": "^11.6.1",
60
+ "react-refresh": "^0.19.0",
61
+ "swc-loader": "^0.2.7",
62
+ "webpack": "^5.111.1",
63
+ "webpack-bundle-analyzer": "^5.4.0",
64
+ "webpack-cli": "^7.2.3",
65
+ "webpack-dev-server": "^6.0.0"
58
66
  },
59
67
  "devDependencies": {
60
- "@asd14/eslint-config": "^14.4.0",
61
- "@asd14/prettier-config": "^1.1.0",
62
- "@asd14/ts-config": "^1.1.0",
63
- "@commitlint/cli": "^19.8.1",
64
- "@commitlint/config-conventional": "^19.8.1",
65
- "@semantic-release/git": "^10.0.1",
68
+ "@asd14/eslint-config": "^15.4.0",
69
+ "@asd14/prettier-config": "^1.5.0",
70
+ "@asd14/semantic-release-config": "^1.0.1",
71
+ "@asd14/ts-config": "^1.6.0",
72
+ "@commitlint/cli": "^21.2.3",
73
+ "@commitlint/config-conventional": "^21.2.3",
74
+ "@tailwindcss/postcss": "^4.3.3",
66
75
  "@types/dotenv-webpack": "^7.0.8",
67
76
  "@types/webpack-bundle-analyzer": "^4.7.0",
68
- "conventional-changelog-conventionalcommits": "^9.1.0",
69
- "eslint": "^9.35.0",
70
- "prettier": "^3.6.2",
71
- "semantic-release": "^24.2.8",
72
- "typescript": "^5.9.2"
77
+ "@xyflow/react": "^12.12.0",
78
+ "eslint": "^9.39.5",
79
+ "prettier": "^3.9.10",
80
+ "semantic-release": "^25.0.9",
81
+ "tailwindcss": "^4.3.3",
82
+ "typescript": "^6.0.3"
73
83
  },
74
84
  "peerDependencies": {
75
85
  "react": "^18 || ^19",
76
86
  "react-dom": "^18 || ^19"
77
87
  },
78
88
  "engines": {
79
- "node": ">=20"
89
+ "node": "^22.22.3 || ^24.15.0 || >=26.0"
80
90
  }
81
91
  }
@@ -1,13 +1,13 @@
1
- export default {
1
+ /**
2
+ * @param {{ mode: "production" | "development" | "none" }} api
3
+ */
4
+ export default ({ mode }) => ({
2
5
  plugins: {
3
6
  "postcss-preset-env": {
4
7
  stage: 2,
5
8
  },
6
- "tailwindcss": {
7
- config: process.env["VB_TAILWIND_CONFIG"],
9
+ "cssnano": mode === "production" && {
10
+ preset: "default",
8
11
  },
9
- // "cssnano": {
10
- // preset: "default",
11
- // },
12
12
  },
13
- }
13
+ })
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Types for the files `../webpack.config.js` teaches `import` to load.
3
+ *
4
+ * @example
5
+ * // tsconfig.json
6
+ * { "compilerOptions": { "types": ["@asd14/vblocks/client"] } }
7
+ */
8
+
9
+ declare module "*.module.css" {
10
+ /**
11
+ * Class names by their camelCase key, `.my-class` is `styles.myClass`.
12
+ *
13
+ * Typed `any` so `styles.myClass` passes `noPropertyAccessFromIndexSignature`.
14
+ */
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const classNames: any
17
+
18
+ export default classNames
19
+ }
20
+
21
+ declare module "*.css" {}
22
+
23
+ /**
24
+ * The URL of the image, a `data:` URL when the build inlines it.
25
+ */
26
+ declare module "*.jpg" {
27
+ const url: string
28
+
29
+ export default url
30
+ }
31
+
32
+ declare module "*.gif" {
33
+ const url: string
34
+
35
+ export default url
36
+ }
37
+
38
+ declare module "*.png" {
39
+ const url: string
40
+
41
+ export default url
42
+ }
43
+
44
+ declare module "*.svg" {
45
+ const url: string
46
+
47
+ export default url
48
+ }
49
+
50
+ /**
51
+ * The URL of the font, a `data:` URL in a single-file build.
52
+ */
53
+ declare module "*.woff" {
54
+ const url: string
55
+
56
+ export default url
57
+ }
58
+
59
+ declare module "*.woff2" {
60
+ const url: string
61
+
62
+ export default url
63
+ }
64
+
65
+ declare module "*.ttf" {
66
+ const url: string
67
+
68
+ export default url
69
+ }
70
+
71
+ declare module "*.eot" {
72
+ const url: string
73
+
74
+ export default url
75
+ }
76
+
77
+ /**
78
+ * The `.env` variables Webpack writes into the bundle. The browser has no
79
+ * other part of Node's `process`.
80
+ *
81
+ * @example
82
+ * const apiUrl = process.env["API_URL"]
83
+ */
84
+ declare const process: {
85
+ env: Record<string, string | undefined>
86
+ }
@@ -7,6 +7,7 @@ import CopyPlugin from "copy-webpack-plugin"
7
7
  import DotenvPlugin from "dotenv-webpack"
8
8
  import HtmlWebPackPlugin from "html-webpack-plugin"
9
9
  import MiniCssExtractPlugin from "mini-css-extract-plugin"
10
+ import webpack from "webpack"
10
11
  import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"
11
12
 
12
13
  const VB_SWC_CONFIG = process.env["VB_SWC_CONFIG"]
@@ -20,8 +21,10 @@ if (!VB_POSTCONFIG_PATH)
20
21
 
21
22
  /**
22
23
  * Load, parse and provide targets defaults for SWC configuration
24
+ *
25
+ * @param {{ hasReactRefresh: boolean }} options
23
26
  */
24
- const loadSwcConfig = () => {
27
+ const loadSwcConfig = ({ hasReactRefresh }) => {
25
28
  const content = fs.readFileSync(VB_SWC_CONFIG, "utf8")
26
29
  const config = /** @type {unknown} */ (JSON.parse(content))
27
30
  const typedConfig = /** @type {import("@swc/core").Config} */ (config)
@@ -29,9 +32,22 @@ const loadSwcConfig = () => {
29
32
  return {
30
33
  ...typedConfig,
31
34
  env: {
32
- targets: "> 0.25, not dead",
35
+ targets: "> 0.25%, not dead",
33
36
  ...typedConfig.env,
34
37
  },
38
+ jsc: {
39
+ ...typedConfig.jsc,
40
+ transform: {
41
+ ...typedConfig.jsc?.transform,
42
+ react: {
43
+ ...typedConfig.jsc?.transform?.react,
44
+ // SWC marks components and their hooks, `ReactRefreshWebpackPlugin`
45
+ // loads the runtime that reads the marks. One without the other
46
+ // breaks the page
47
+ ...(hasReactRefresh ? { development: true, refresh: true } : {}),
48
+ },
49
+ },
50
+ },
35
51
  }
36
52
  }
37
53
 
@@ -55,12 +71,83 @@ const createCssModuleConfig = isProduction => ({
55
71
  localIdentName: isProduction ? "[hash:base64:8]" : "[local]__[hash:base64:5]",
56
72
  })
57
73
 
74
+ /**
75
+ * Replace every script, stylesheet and icon tag of the HTML with the content
76
+ * of the file it points at, then drop that file from the output.
77
+ *
78
+ * Lives in this file so an ejected config stays one file.
79
+ *
80
+ * @param {import("webpack").Compiler} compiler
81
+ */
82
+ const inlineAssets = compiler => {
83
+ compiler.hooks.compilation.tap("inlineAssets", compilation => {
84
+ /** @param {string} name */
85
+ const takeAsset = name => {
86
+ const content = compilation.getAsset(name)?.source.source() ?? ""
87
+
88
+ compilation.deleteAsset(name)
89
+
90
+ return content
91
+ }
92
+
93
+ /** @param {import("html-webpack-plugin").HtmlTagObject} tag */
94
+ const inlineTag = tag => {
95
+ const { src, href, rel } = tag.attributes
96
+
97
+ if (tag.tagName === "script" && typeof src === "string")
98
+ return {
99
+ ...tag,
100
+ attributes: {},
101
+ // The HTML parser reads `</script` and `<!--` inside a script, a
102
+ // `\x3C` means the same `<` to JavaScript and nothing to the parser
103
+ innerHTML: takeAsset(src)
104
+ .toString()
105
+ .replaceAll("</script", String.raw`\x3C/script`)
106
+ .replaceAll("<!--", String.raw`\x3C!--`),
107
+ }
108
+
109
+ if (rel === "stylesheet" && typeof href === "string")
110
+ return {
111
+ ...tag,
112
+ tagName: "style",
113
+ voidTag: false,
114
+ attributes: {},
115
+ innerHTML: takeAsset(href)
116
+ .toString()
117
+ .replaceAll("</style", String.raw`<\/style`),
118
+ }
119
+
120
+ if (rel === "icon" && typeof href === "string")
121
+ return {
122
+ ...tag,
123
+ attributes: {
124
+ rel,
125
+ // `bin/vblocks.sh` only ever passes a `favicon.svg`
126
+ href: `data:image/svg+xml;base64,${Buffer.from(takeAsset(href)).toString("base64")}`,
127
+ },
128
+ }
129
+
130
+ return tag
131
+ }
132
+
133
+ HtmlWebPackPlugin.getHooks(compilation).alterAssetTagGroups.tap(
134
+ "inlineAssets",
135
+ data => ({
136
+ ...data,
137
+ headTags: data.headTags.map(tag => inlineTag(tag)),
138
+ bodyTags: data.bodyTags.map(tag => inlineTag(tag)),
139
+ })
140
+ )
141
+ })
142
+ }
143
+
58
144
  /**
59
145
  * @typedef {Object} VBlocksEnvironment
60
146
  *
61
147
  * @property {string} entryDir - Target directory from where vblocks cmd was called
62
148
  * @property {string} robotsPath - Path to robots.txt copied to the output directory
63
149
  * @property {string} faviconPath - Path to favicon.ico copied to the output directory
150
+ * @property {boolean} [single] - Build one standalone index.html, every asset inlined
64
151
  */
65
152
 
66
153
  /**
@@ -74,10 +161,12 @@ const createCssModuleConfig = isProduction => ({
74
161
  * @param {VBlocksProps} props
75
162
  * @returns {WebpackConfiguration}
76
163
  */
77
- export default ({ entryDir, robotsPath, faviconPath }, { mode }) => {
164
+ export default ({ entryDir, robotsPath, faviconPath, single }, { mode }) => {
78
165
  const isProduction = mode === "production"
79
166
  const isServe = process.argv[2] == "serve"
80
- const swcConfig = loadSwcConfig()
167
+ const isSingle = Boolean(single) && !isServe
168
+ const hasReactRefresh = isServe && mode === "development"
169
+ const swcConfig = loadSwcConfig({ hasReactRefresh })
81
170
  const commonPostCSSConfigs = createCssModuleConfig(isProduction)
82
171
 
83
172
  const paths = {
@@ -102,7 +191,8 @@ export default ({ entryDir, robotsPath, faviconPath }, { mode }) => {
102
191
  mode: mode,
103
192
 
104
193
  output: {
105
- publicPath: "/",
194
+ // A standalone file opens from disk, where "/" is the filesystem root
195
+ publicPath: isSingle ? "" : "/",
106
196
  path: paths.output,
107
197
  // Cannot use 'contenthash' when hot reloading is enabled.
108
198
  filename: isServe ? "js/[name].js" : "js/[name].[contenthash].js",
@@ -201,19 +291,27 @@ export default ({ entryDir, robotsPath, faviconPath }, { mode }) => {
201
291
  },
202
292
  {
203
293
  test: /\.(jpg|gif|png|svg)$/,
204
- type: "asset",
205
- parser: {
206
- dataUrlCondition: {
207
- maxSize: 10 * 1024,
208
- },
209
- },
294
+ ...(isSingle
295
+ ? { type: "asset/inline" }
296
+ : {
297
+ type: "asset",
298
+ parser: {
299
+ dataUrlCondition: {
300
+ maxSize: 10 * 1024,
301
+ },
302
+ },
303
+ }),
210
304
  },
211
305
  {
212
306
  test: /\.(woff|woff2|ttf|eot)$/,
213
- type: "asset/resource",
214
- generator: {
215
- filename: "fonts/[name][ext]",
216
- },
307
+ ...(isSingle
308
+ ? { type: "asset/inline" }
309
+ : {
310
+ type: "asset/resource",
311
+ generator: {
312
+ filename: "fonts/[name][ext]",
313
+ },
314
+ }),
217
315
  },
218
316
  ],
219
317
  },
@@ -243,6 +341,9 @@ export default ({ entryDir, robotsPath, faviconPath }, { mode }) => {
243
341
  new HtmlWebPackPlugin({
244
342
  template: paths.htmlTemplate,
245
343
  favicon: paths.favicon,
344
+ // An inline script ignores `defer` and runs where it stands, so it
345
+ // must stand after the element the app renders into
346
+ inject: isSingle ? "body" : "head",
246
347
  }),
247
348
 
248
349
  new MiniCssExtractPlugin({
@@ -253,21 +354,27 @@ export default ({ entryDir, robotsPath, faviconPath }, { mode }) => {
253
354
  path: paths.dotenv,
254
355
  }),
255
356
 
256
- new CopyPlugin({
257
- patterns: [
258
- {
259
- from: paths.robots,
260
- to: "robots.txt",
261
- },
262
- ],
263
- }),
357
+ !isSingle &&
358
+ new CopyPlugin({
359
+ patterns: [
360
+ {
361
+ from: paths.robots,
362
+ to: "robots.txt",
363
+ },
364
+ ],
365
+ }),
366
+
367
+ // A dynamic `import()` would otherwise emit a second file
368
+ isSingle && new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
369
+
370
+ isSingle && inlineAssets,
264
371
 
265
372
  isServe &&
266
373
  new BundleAnalyzerPlugin({
267
374
  openAnalyzer: false,
268
375
  }),
269
376
 
270
- isServe && new ReactRefreshWebpackPlugin(),
377
+ hasReactRefresh && new ReactRefreshWebpackPlugin(),
271
378
  ].filter(Boolean),
272
379
 
273
380
  resolve: {
@@ -285,7 +392,7 @@ export default ({ entryDir, robotsPath, faviconPath }, { mode }) => {
285
392
  runtimeChunk: false,
286
393
 
287
394
  // Extract node_module dependencies into a separate chunk for better caching
288
- splitChunks: {
395
+ splitChunks: !isSingle && {
289
396
  chunks: "all",
290
397
  cacheGroups: {
291
398
  react: {
@@ -305,7 +412,8 @@ export default ({ entryDir, robotsPath, faviconPath }, { mode }) => {
305
412
  },
306
413
 
307
414
  performance: {
308
- hints: isProduction ? "warning" : false,
415
+ // A standalone file is over the size limit by design
416
+ hints: isProduction && !isSingle ? "warning" : false,
309
417
  },
310
418
  }
311
419
  }
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ .env
@@ -0,0 +1,4 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: system-ui, sans-serif;
4
+ }