@anaemia/bundler 0.3.7 → 0.5.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/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/aliases.js +1 -1
- package/dist/analyzer/ast-utils.d.ts +5 -0
- package/dist/analyzer/ast-utils.d.ts.map +1 -0
- package/dist/analyzer/ast-utils.js +16 -0
- package/dist/analyzer/ast-walker.d.ts +15 -0
- package/dist/analyzer/ast-walker.d.ts.map +1 -0
- package/dist/analyzer/ast-walker.js +43 -0
- package/dist/analyzer/checks/env-access.d.ts +3 -0
- package/dist/analyzer/checks/env-access.d.ts.map +1 -0
- package/dist/analyzer/checks/env-access.js +63 -0
- package/dist/analyzer/checks/route-metadata.d.ts +12 -0
- package/dist/analyzer/checks/route-metadata.d.ts.map +1 -0
- package/dist/analyzer/checks/route-metadata.js +74 -0
- package/dist/analyzer/checks/server-functions.d.ts +3 -0
- package/dist/analyzer/checks/server-functions.d.ts.map +1 -0
- package/dist/analyzer/checks/server-functions.js +75 -0
- package/dist/analyzer/index.d.ts +7 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +49 -0
- package/dist/analyzer/parser.d.ts +4 -0
- package/dist/analyzer/parser.d.ts.map +1 -0
- package/dist/analyzer/parser.js +96 -0
- package/dist/analyzer/types.d.ts +48 -0
- package/dist/analyzer/types.d.ts.map +1 -0
- package/dist/analyzer/types.js +1 -0
- package/dist/env-loader.d.ts +2 -0
- package/dist/env-loader.d.ts.map +1 -0
- package/dist/env-loader.js +10 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -15
- package/dist/optimization.d.ts.map +1 -1
- package/dist/optimization.js +17 -4
- package/dist/plugins/babel-transform-server.d.ts.map +1 -1
- package/dist/plugins/babel-transform-server.js +1 -4
- package/dist/router/generate-entry.d.ts.map +1 -1
- package/dist/router/generate-entry.js +3 -3
- package/dist/router/generate-server-routes.d.ts.map +1 -1
- package/dist/router/generate-server-routes.js +15 -5
- package/dist/router/manifest.d.ts +3 -2
- package/dist/router/manifest.d.ts.map +1 -1
- package/dist/router/manifest.js +20 -20
- package/dist/router/scan.d.ts.map +1 -1
- package/dist/router/scan.js +5 -6
- package/dist/rules.d.ts +16 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +37 -5
- package/package.json +11 -3
- package/src/aliases.ts +2 -2
- package/src/analyzer/ast-utils.ts +22 -0
- package/src/analyzer/ast-walker.ts +63 -0
- package/src/analyzer/checks/env-access.ts +77 -0
- package/src/analyzer/checks/route-metadata.ts +91 -0
- package/src/analyzer/checks/server-functions.ts +85 -0
- package/src/analyzer/index.ts +70 -0
- package/src/analyzer/parser.ts +103 -0
- package/src/analyzer/types.ts +55 -0
- package/src/env-loader.ts +13 -0
- package/src/index.ts +119 -19
- package/src/optimization.ts +18 -5
- package/src/plugins/babel-transform-server.ts +1 -4
- package/src/plugins/rspack-manifest-hydration.ts +3 -3
- package/src/router/generate-entry.ts +15 -6
- package/src/router/generate-server-routes.ts +16 -5
- package/src/router/manifest.ts +24 -38
- package/src/router/scan.ts +9 -10
- package/src/rules.ts +48 -8
- package/test/analyzer.test.mjs +308 -0
- package/test/rspack-config.test.mjs +5 -2
- package/test/server-functions.test.mjs +25 -22
- package/tsconfig.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { rspack } from "@rspack/core";
|
|
2
|
-
import path from "path";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import pc from "picocolors";
|
|
6
7
|
import clientServerFnTransform from "./plugins/babel-transform-server.js";
|
|
7
8
|
import serverHashInjector from "./plugins/babel-hash-injector-server.js";
|
|
8
9
|
import { AnaemiaManifestHydrationPlugin } from "./plugins/rspack-manifest-hydration.js";
|
|
@@ -11,53 +12,93 @@ import { writeManifest } from "./router/manifest.js";
|
|
|
11
12
|
import { generateRouterEntry } from "./router/generate-entry.js";
|
|
12
13
|
import { generateServerRoutes } from "./router/generate-server-routes.js";
|
|
13
14
|
import { getAliases } from "./aliases.js";
|
|
14
|
-
import { createStyleRules, createBabelRule } from "./rules.js";
|
|
15
|
+
import { createStyleRules, createBabelRule, createAssetRules } from "./rules.js";
|
|
15
16
|
import { getClientOptimization, getPerformanceProfile } from "./optimization.js";
|
|
17
|
+
import loadEnvFiles from "./env-loader.js";
|
|
18
|
+
import { analyzeApp } from "./analyzer/index.js";
|
|
16
19
|
const require = createRequire(import.meta.url);
|
|
17
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
21
|
const __dirname = path.dirname(__filename);
|
|
19
22
|
export async function getRspackConfig(appRoot, config = {}) {
|
|
20
23
|
const isDev = process.env.NODE_ENV !== "production";
|
|
21
|
-
|
|
24
|
+
loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
|
|
25
|
+
// run the analyzer to collect route metadata and other information about the app that we can use to optimize the build
|
|
26
|
+
const analysis = await analyzeApp(appRoot, {
|
|
27
|
+
mode: isDev ? "development" : "production",
|
|
28
|
+
});
|
|
29
|
+
// flush diagnostics to console
|
|
30
|
+
const tag = pc.dim("[anaemia-analyzer]");
|
|
31
|
+
for (const diagnostic of analysis.diagnostics) {
|
|
32
|
+
const prefix = diagnostic.severity === "error"
|
|
33
|
+
? pc.red("✖ [error]")
|
|
34
|
+
: diagnostic.severity === "warning"
|
|
35
|
+
? pc.yellow("⚠ [warning]")
|
|
36
|
+
: pc.cyan("› [info]");
|
|
37
|
+
const loc = diagnostic.line ? pc.dim(`:${diagnostic.line}`) : "";
|
|
38
|
+
const file = pc.bold(diagnostic.filePath);
|
|
39
|
+
const msg = diagnostic.severity === "error"
|
|
40
|
+
? pc.red(diagnostic.message)
|
|
41
|
+
: diagnostic.severity === "warning"
|
|
42
|
+
? pc.yellow(diagnostic.message)
|
|
43
|
+
: diagnostic.message;
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.log(`${tag} ${prefix} ${file}${loc} - ${msg}`);
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
if (diagnostic.help)
|
|
48
|
+
console.log(` ${pc.dim(`hint: ${diagnostic.help}`)}`);
|
|
49
|
+
}
|
|
22
50
|
const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
|
|
23
51
|
const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
|
|
24
52
|
const routes = await scanRoutes(appRoot);
|
|
25
53
|
const serverRoutes = scanServerRoutes(appRoot);
|
|
26
|
-
writeManifest(appRoot, routes);
|
|
54
|
+
writeManifest(appRoot, routes, analysis.routeMetadata);
|
|
27
55
|
const frameworkInternalDir = path.resolve(appRoot, "./.anaemia");
|
|
28
56
|
if (!fs.existsSync(frameworkInternalDir)) {
|
|
29
57
|
fs.mkdirSync(frameworkInternalDir, { recursive: true });
|
|
30
58
|
}
|
|
59
|
+
// bootstrap config entries and generate necessary files for the router based on the scanned routes
|
|
31
60
|
const entryFile = generateRouterEntry(appRoot, routes);
|
|
32
61
|
const serverRoutesFile = generateServerRoutes(appRoot, serverRoutes);
|
|
33
62
|
const styleRules = createStyleRules(config);
|
|
63
|
+
const assetRules = createAssetRules(isDev);
|
|
64
|
+
// allow users to inject additional babel plugins via the config, which is useful for things like macros or other code transforms that need to run at compile time
|
|
34
65
|
const extraClientBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.client ?? []) ?? [];
|
|
35
66
|
const extraServerBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.server ?? []) ?? [];
|
|
36
67
|
const solidRefreshPlugin = [require.resolve("solid-refresh/babel"), { bundler: "rspack-esm", jsx: false }];
|
|
37
|
-
|
|
68
|
+
/**
|
|
69
|
+
* env processing:
|
|
70
|
+
*
|
|
71
|
+
* - we inject some default env vars like MODE, DEV, and PROD for convenience
|
|
72
|
+
* - for the server, we expose all env vars
|
|
73
|
+
* - for the client, we only expose vars that start with PUBLIC_, as well as the same defaults
|
|
74
|
+
* - users can also define additional compile-time constants via config.define.client and config.define.server, which are merged into the rspack DefinePlugin config
|
|
75
|
+
*/
|
|
38
76
|
const serverEnv = {
|
|
39
77
|
MODE: JSON.stringify(process.env.NODE_ENV || "development"),
|
|
40
78
|
DEV: JSON.stringify(isDev),
|
|
41
79
|
PROD: JSON.stringify(!isDev),
|
|
42
80
|
};
|
|
43
|
-
for (const key in
|
|
44
|
-
serverEnv[key] = JSON.stringify(
|
|
81
|
+
for (const key in process.env) {
|
|
82
|
+
serverEnv[key] = JSON.stringify(process.env[key]);
|
|
45
83
|
}
|
|
46
84
|
const clientEnv = {
|
|
47
85
|
MODE: JSON.stringify(process.env.NODE_ENV || "development"),
|
|
48
86
|
DEV: JSON.stringify(isDev),
|
|
49
87
|
PROD: JSON.stringify(!isDev),
|
|
50
88
|
};
|
|
51
|
-
for (const key in
|
|
89
|
+
for (const key in process.env) {
|
|
52
90
|
if (key.startsWith("PUBLIC_")) {
|
|
53
|
-
clientEnv[key] = JSON.stringify(
|
|
91
|
+
clientEnv[key] = JSON.stringify(process.env[key]);
|
|
54
92
|
}
|
|
55
93
|
}
|
|
94
|
+
// shared resolve config
|
|
95
|
+
// we set up some shared resolve config between client and server here, like extension aliases and path aliases
|
|
56
96
|
const sharedResolve = {
|
|
57
97
|
extensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".scss", ".css"],
|
|
58
98
|
extensionAlias: { ".js": [".ts", ".js"], ".jsx": [".tsx", ".jsx"] },
|
|
59
99
|
alias: { "anaemia-user-app": entryFile, ...getAliases(appRoot) },
|
|
60
100
|
};
|
|
101
|
+
// client and server configurations
|
|
61
102
|
let clientConfig = {
|
|
62
103
|
name: "client",
|
|
63
104
|
context: appRoot,
|
|
@@ -84,7 +125,14 @@ export async function getRspackConfig(appRoot, config = {}) {
|
|
|
84
125
|
"solid-refresh": require.resolve("solid-refresh"),
|
|
85
126
|
[path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(coreRuntimeDir, "./dist/runtime/context.browser.js"),
|
|
86
127
|
},
|
|
87
|
-
fallback: {
|
|
128
|
+
fallback: {
|
|
129
|
+
async_hooks: false,
|
|
130
|
+
"node:async_hooks": false,
|
|
131
|
+
fs: false,
|
|
132
|
+
"node:fs": false,
|
|
133
|
+
path: false,
|
|
134
|
+
"node:path": false,
|
|
135
|
+
},
|
|
88
136
|
},
|
|
89
137
|
devServer: isDev
|
|
90
138
|
? {
|
|
@@ -97,9 +145,17 @@ export async function getRspackConfig(appRoot, config = {}) {
|
|
|
97
145
|
}
|
|
98
146
|
: undefined,
|
|
99
147
|
plugins: [
|
|
100
|
-
new rspack.HtmlRspackPlugin({
|
|
148
|
+
new rspack.HtmlRspackPlugin({
|
|
149
|
+
template: path.resolve(appRoot, "./index.html"),
|
|
150
|
+
filename: "index.html",
|
|
151
|
+
inject: false,
|
|
152
|
+
}),
|
|
101
153
|
new rspack.DefinePlugin({
|
|
102
|
-
__ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({
|
|
154
|
+
__ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({
|
|
155
|
+
port: config.port,
|
|
156
|
+
assets: config.assets,
|
|
157
|
+
styles: config.styles,
|
|
158
|
+
}),
|
|
103
159
|
...config.define?.client,
|
|
104
160
|
"import.meta.env": clientEnv,
|
|
105
161
|
}),
|
|
@@ -118,8 +174,13 @@ export async function getRspackConfig(appRoot, config = {}) {
|
|
|
118
174
|
parser: { "css/auto": { namedExports: false } },
|
|
119
175
|
rules: [
|
|
120
176
|
styleRules.client,
|
|
177
|
+
...assetRules.client,
|
|
121
178
|
{
|
|
122
|
-
...createBabelRule({
|
|
179
|
+
...createBabelRule({
|
|
180
|
+
isServer: false,
|
|
181
|
+
isDev,
|
|
182
|
+
plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins],
|
|
183
|
+
}),
|
|
123
184
|
exclude: (modulePath) => {
|
|
124
185
|
if (modulePath.includes("@anaemia") && modulePath.includes("core"))
|
|
125
186
|
return false;
|
|
@@ -137,7 +198,13 @@ export async function getRspackConfig(appRoot, config = {}) {
|
|
|
137
198
|
context: appRoot,
|
|
138
199
|
target: "node",
|
|
139
200
|
entry: { server: path.resolve(runtimeDir, "entry-server.jsx") },
|
|
140
|
-
output: {
|
|
201
|
+
output: {
|
|
202
|
+
path: path.resolve(appRoot, "./dist/server"),
|
|
203
|
+
filename: "index.js",
|
|
204
|
+
module: true,
|
|
205
|
+
chunkFormat: "module",
|
|
206
|
+
chunkLoading: "import",
|
|
207
|
+
},
|
|
141
208
|
optimization: { nodeEnv: false },
|
|
142
209
|
resolve: {
|
|
143
210
|
...sharedResolve,
|
|
@@ -156,8 +223,13 @@ export async function getRspackConfig(appRoot, config = {}) {
|
|
|
156
223
|
parser: { "css/auto": { namedExports: false } },
|
|
157
224
|
rules: [
|
|
158
225
|
styleRules.server,
|
|
226
|
+
...assetRules.server,
|
|
159
227
|
{
|
|
160
|
-
...createBabelRule({
|
|
228
|
+
...createBabelRule({
|
|
229
|
+
isServer: true,
|
|
230
|
+
isDev,
|
|
231
|
+
plugins: [serverHashInjector, ...extraServerBabelPlugins],
|
|
232
|
+
}),
|
|
161
233
|
exclude: (modulePath) => {
|
|
162
234
|
if (modulePath.includes("@anaemia") && modulePath.includes("core"))
|
|
163
235
|
return false;
|
|
@@ -169,6 +241,7 @@ export async function getRspackConfig(appRoot, config = {}) {
|
|
|
169
241
|
],
|
|
170
242
|
},
|
|
171
243
|
};
|
|
244
|
+
// allow plugins to modify the client and server configurations before they are returned
|
|
172
245
|
for (const plugin of config.plugins ?? []) {
|
|
173
246
|
if (plugin.clientRspackConfig)
|
|
174
247
|
clientConfig = plugin.clientRspackConfig(clientConfig);
|
|
@@ -179,3 +252,4 @@ export async function getRspackConfig(appRoot, config = {}) {
|
|
|
179
252
|
}
|
|
180
253
|
export { scanRoutes } from "./router/scan.js";
|
|
181
254
|
export { writeManifest } from "./router/manifest.js";
|
|
255
|
+
export { analyzeApp, collectAnalyzerFiles, parseAnalyzerFile, walkAst } from "./analyzer/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimization.d.ts","sourceRoot":"","sources":["../src/optimization.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"optimization.d.ts","sourceRoot":"","sources":["../src/optimization.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;uBAgDwhI,CAAC;;;;;EAP5kI;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO;;;;;;;;EAInD"}
|
package/dist/optimization.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { rspack } from "@rspack/core";
|
|
2
2
|
export function getClientOptimization(isDev) {
|
|
3
3
|
return {
|
|
4
|
-
sideEffects:
|
|
5
|
-
usedExports:
|
|
4
|
+
sideEffects: !isDev,
|
|
5
|
+
usedExports: !isDev,
|
|
6
6
|
splitChunks: isDev
|
|
7
7
|
? false
|
|
8
8
|
: {
|
|
@@ -13,7 +13,7 @@ export function getClientOptimization(isDev) {
|
|
|
13
13
|
framework: {
|
|
14
14
|
chunks: "all",
|
|
15
15
|
name: "framework",
|
|
16
|
-
test: /[\\/]node_modules[\\/](solid-js|@solidjs
|
|
16
|
+
test: /[\\/]node_modules[\\/](solid-js|@solidjs)[\\/]/,
|
|
17
17
|
priority: 40,
|
|
18
18
|
enforce: true,
|
|
19
19
|
},
|
|
@@ -25,7 +25,20 @@ export function getClientOptimization(isDev) {
|
|
|
25
25
|
},
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
|
-
minimizer: isDev
|
|
28
|
+
minimizer: isDev
|
|
29
|
+
? []
|
|
30
|
+
: [
|
|
31
|
+
new rspack.SwcJsMinimizerRspackPlugin({
|
|
32
|
+
minimizerOptions: {
|
|
33
|
+
mangle: {
|
|
34
|
+
keep_fnames: true,
|
|
35
|
+
},
|
|
36
|
+
compress: {
|
|
37
|
+
keep_fnames: true,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
29
42
|
};
|
|
30
43
|
}
|
|
31
44
|
export function getPerformanceProfile(isDev) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"babel-transform-server.d.ts","sourceRoot":"","sources":["../../src/plugins/babel-transform-server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAY,SAAS,EAAE,UAAU,EAAE,KAAK,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAExF,UAAU,WAAY,SAAQ,UAAU;IACtC,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAAE,KAAK,EAAE,OAAO,UAAU,CAAA;CAAE,GAAG,SAAS,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"babel-transform-server.d.ts","sourceRoot":"","sources":["../../src/plugins/babel-transform-server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAY,SAAS,EAAE,UAAU,EAAE,KAAK,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAExF,UAAU,WAAY,SAAQ,UAAU;IACtC,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAAE,KAAK,EAAE,OAAO,UAAU,CAAA;CAAE,GAAG,SAAS,CAAC,WAAW,CAAC,CA0IlH"}
|
|
@@ -19,14 +19,11 @@ export default function clientServerFnTransform({ types: t }) {
|
|
|
19
19
|
if (t.isIdentifier(path.node.callee) && path.node.callee.name === "runOnServer") {
|
|
20
20
|
state.hasRunOnServer = true;
|
|
21
21
|
const filename = state.file.opts.filename || "unknown";
|
|
22
|
-
const serverFunctionCallback = path.node.arguments[0];
|
|
23
22
|
const explicitId = path.node.arguments[1];
|
|
24
23
|
const functionHash = t.isStringLiteral(explicitId)
|
|
25
24
|
? explicitId.value
|
|
26
25
|
: createServerFunctionId(filename, path.node.start);
|
|
27
|
-
|
|
28
|
-
path.get('arguments.0').remove();
|
|
29
|
-
}
|
|
26
|
+
path.get('arguments.0').remove();
|
|
30
27
|
// this whole thing is just magic bro
|
|
31
28
|
// wtf is this ast manipulation
|
|
32
29
|
path.replaceWith(t.callExpression(t.arrowFunctionExpression([], t.blockStatement([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-entry.d.ts","sourceRoot":"","sources":["../../src/router/generate-entry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"generate-entry.d.ts","sourceRoot":"","sources":["../../src/router/generate-entry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AA0HpD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,MAAM,CAqMzF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import { transform } from "sucrase";
|
|
4
4
|
function buildTree(routes, strippedLayouts, routeIndices, routesDir, allLayouts, parentPrefix) {
|
|
5
5
|
const nodes = [];
|
|
@@ -66,7 +66,7 @@ function renderTree(nodes, indent = 6) {
|
|
|
66
66
|
}
|
|
67
67
|
return `${pad}<Route path="${routePath}" component={Route${node.routeIdx}Wrapped} />`;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
const layoutPath = node.relativePath;
|
|
70
70
|
const inner = renderTree(node.children, indent + 2);
|
|
71
71
|
return [`${pad}<Route path="${layoutPath}" component={Layout${node.layoutIdx}}>`, inner, `${pad}</Route>`].join("\n");
|
|
72
72
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-server-routes.d.ts","sourceRoot":"","sources":["../../src/router/generate-server-routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"generate-server-routes.d.ts","sourceRoot":"","sources":["../../src/router/generate-server-routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAyCxF"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { transform } from "sucrase";
|
|
3
4
|
export function generateServerRoutes(appRoot, routes) {
|
|
4
5
|
const outDir = path.resolve(appRoot, "./.anaemia");
|
|
5
|
-
const
|
|
6
|
+
const isTs = fs.existsSync(path.resolve(appRoot, "tsconfig.json"));
|
|
7
|
+
const ext = isTs ? "ts" : "js";
|
|
8
|
+
const outPath = path.resolve(outDir, "./__anaemia_server_routes__." + ext);
|
|
6
9
|
const imports = routes.map((r, i) => `import * as ServerRoute${i} from "${r.filePath}";`).join("\n");
|
|
7
10
|
const registrations = routes.map((r, i) => ` registerRoute(app, "${r.urlPattern}", ServerRoute${i});`).join("\n");
|
|
8
|
-
const
|
|
11
|
+
const rawCode = `
|
|
9
12
|
// @ts-nocheck
|
|
10
13
|
// auto-generated by anaemia - do not edit!!
|
|
11
14
|
import type { Hono } from "hono";
|
|
@@ -25,6 +28,13 @@ export function registerServerRoutes(app: Hono) {
|
|
|
25
28
|
${registrations}
|
|
26
29
|
}
|
|
27
30
|
`.trimStart();
|
|
28
|
-
|
|
31
|
+
const finalCode = isTs
|
|
32
|
+
? rawCode
|
|
33
|
+
: transform(rawCode.replace("// @ts-nocheck\n", ""), {
|
|
34
|
+
transforms: ["typescript", "jsx"],
|
|
35
|
+
jsxRuntime: "preserve",
|
|
36
|
+
production: true,
|
|
37
|
+
}).code;
|
|
38
|
+
fs.writeFileSync(outPath, finalCode);
|
|
29
39
|
return outPath;
|
|
30
40
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { RouteManifestEntry } from "
|
|
2
|
-
|
|
1
|
+
import type { RouteManifestEntry } from "../router/scan.js";
|
|
2
|
+
import type { RouteMetadata } from "../analyzer/checks/route-metadata.js";
|
|
3
|
+
export declare function writeManifest(appRoot: string, routes: RouteManifestEntry[], routeMetadata: RouteMetadata[]): void;
|
|
3
4
|
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/router/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/router/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAE1E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,aAAa,EAAE,aAAa,EAAE,QAsB1G"}
|
package/dist/router/manifest.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
export function writeManifest(appRoot, routes) {
|
|
4
|
-
const
|
|
5
|
-
for (const route of routes) {
|
|
6
|
-
if (route.filePath.endsWith("404.tsx")) {
|
|
7
|
-
errors["404"] = route.urlPattern;
|
|
8
|
-
}
|
|
9
|
-
if (route.filePath.endsWith("500.tsx")) {
|
|
10
|
-
errors["500"] = route.urlPattern;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
const conventionalRoutes = routes.filter((r) => !r.filePath.endsWith("404.tsx") && !r.filePath.endsWith("500.tsx"));
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function writeManifest(appRoot, routes, routeMetadata) {
|
|
4
|
+
const metadataMap = new Map(routeMetadata.map((m) => [path.resolve(appRoot, m.filePath), m]));
|
|
14
5
|
const manifest = {
|
|
15
|
-
routes:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
routes: routes.map((route) => {
|
|
7
|
+
const meta = metadataMap.get(route.filePath);
|
|
8
|
+
return {
|
|
9
|
+
...route,
|
|
10
|
+
isStatic: meta?.isStatic ?? false,
|
|
11
|
+
hasLoader: meta?.hasLoader ?? false,
|
|
12
|
+
hasGuard: meta?.hasGuard ?? false,
|
|
13
|
+
serverFunctionIds: meta?.serverFunctionIds ?? [],
|
|
14
|
+
};
|
|
15
|
+
}),
|
|
16
|
+
chunks: {},
|
|
19
17
|
};
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
fs.
|
|
18
|
+
const manifestPath = path.resolve(appRoot, "./dist/route-manifest.json");
|
|
19
|
+
const manifestDir = path.dirname(manifestPath);
|
|
20
|
+
if (!fs.existsSync(manifestDir))
|
|
21
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
23
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/router/scan.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;AAezD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/router/scan.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;AAezD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAgBpE;AAUD,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAiE/E"}
|
package/dist/router/scan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { glob } from "glob";
|
|
2
|
-
import path from "path";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import { createJiti } from "jiti";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import { getAliases } from "../aliases.js";
|
|
@@ -17,9 +17,7 @@ export function scanServerRoutes(appRoot) {
|
|
|
17
17
|
const files = glob.sync("**/_route.{ts,tsx,js,jsx}", { cwd: routesDir, posix: true });
|
|
18
18
|
return files.map((file) => {
|
|
19
19
|
const dir = path.dirname(file);
|
|
20
|
-
const normalizedDir = dir
|
|
21
|
-
.replace(/\[\.\.\.(.+?)\]/g, "*")
|
|
22
|
-
.replace(/\[(.+?)\]/g, ":$1");
|
|
20
|
+
const normalizedDir = dir.replace(/\[\.\.\.(.+?)\]/g, "*").replace(/\[(.+?)\]/g, ":$1");
|
|
23
21
|
const urlPattern = normalizedDir === "." ? "/" : `/${normalizedDir}`;
|
|
24
22
|
return {
|
|
25
23
|
urlPattern,
|
|
@@ -47,7 +45,7 @@ export async function scanRoutes(appRoot) {
|
|
|
47
45
|
let layoutGuards = [];
|
|
48
46
|
try {
|
|
49
47
|
const layoutModule = (await jiti.import(resolveConfigPath(absolutePath)));
|
|
50
|
-
if (layoutModule
|
|
48
|
+
if (layoutModule.config?.guards) {
|
|
51
49
|
layoutGuards = layoutModule.config.guards;
|
|
52
50
|
}
|
|
53
51
|
}
|
|
@@ -73,7 +71,7 @@ export async function scanRoutes(appRoot) {
|
|
|
73
71
|
let pageGuards = [];
|
|
74
72
|
try {
|
|
75
73
|
const pageModule = (await jiti.import(resolveConfigPath(absolutePagePath)));
|
|
76
|
-
if (pageModule
|
|
74
|
+
if (pageModule.config?.guards) {
|
|
77
75
|
pageGuards = pageModule.config.guards;
|
|
78
76
|
}
|
|
79
77
|
}
|
|
@@ -148,6 +146,7 @@ function parseFilePath(file) {
|
|
|
148
146
|
function resolveLayoutChain(dir, layoutMap) {
|
|
149
147
|
const layouts = [];
|
|
150
148
|
let current = dir;
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
151
150
|
while (true) {
|
|
152
151
|
const layoutEntry = layoutMap.get(current);
|
|
153
152
|
if (layoutEntry)
|
package/dist/rules.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnaemiaConfig } from "@anaemia/core";
|
|
2
|
-
import { PluginItem } from "@babel/core";
|
|
2
|
+
import type { PluginItem } from "@babel/core";
|
|
3
|
+
import type { RuleSetRule } from "@rspack/core";
|
|
3
4
|
export declare function createStyleRules(config: AnaemiaConfig): {
|
|
4
5
|
client: {
|
|
5
6
|
test: RegExp;
|
|
@@ -10,6 +11,11 @@ export declare function createStyleRules(config: AnaemiaConfig): {
|
|
|
10
11
|
api: string;
|
|
11
12
|
};
|
|
12
13
|
}[];
|
|
14
|
+
parser: {
|
|
15
|
+
cssModules: {
|
|
16
|
+
localIdentName: string;
|
|
17
|
+
};
|
|
18
|
+
} | undefined;
|
|
13
19
|
};
|
|
14
20
|
server: {
|
|
15
21
|
test: RegExp;
|
|
@@ -25,6 +31,11 @@ export declare function createStyleRules(config: AnaemiaConfig): {
|
|
|
25
31
|
api: string;
|
|
26
32
|
};
|
|
27
33
|
}[];
|
|
34
|
+
parser: {
|
|
35
|
+
cssModules: {
|
|
36
|
+
localIdentName: string;
|
|
37
|
+
};
|
|
38
|
+
} | undefined;
|
|
28
39
|
};
|
|
29
40
|
};
|
|
30
41
|
export declare function createBabelRule({ isServer, isDev, plugins, }: {
|
|
@@ -45,4 +56,8 @@ export declare function createBabelRule({ isServer, isDev, plugins, }: {
|
|
|
45
56
|
};
|
|
46
57
|
}[];
|
|
47
58
|
};
|
|
59
|
+
export declare function createAssetRules(isDev: boolean): {
|
|
60
|
+
client: RuleSetRule[];
|
|
61
|
+
server: RuleSetRule[];
|
|
62
|
+
};
|
|
48
63
|
//# sourceMappingURL=rules.d.ts.map
|
package/dist/rules.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBrD;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,KAAK,EACL,OAAY,GACb,EAAE;IACD,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;CACxB;;;;;;;;;;;;;EAkBA;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO;;;EAsC9C"}
|
package/dist/rules.js
CHANGED
|
@@ -4,21 +4,21 @@ export function createStyleRules(config) {
|
|
|
4
4
|
const useSass = config.styles?.sass !== false;
|
|
5
5
|
const useModules = config.styles?.modules ?? true;
|
|
6
6
|
const baseLoaders = useSass ? [{ loader: require.resolve("sass-loader"), options: { api: "modern" } }] : [];
|
|
7
|
+
const localIdentName = config.styles?.modulesLocalIdentName ?? "[name]__[local]__[hash:base64:5]";
|
|
8
|
+
const cssParser = useModules ? { cssModules: { localIdentName } } : undefined;
|
|
7
9
|
return {
|
|
8
10
|
client: {
|
|
9
11
|
test: /\.(c|sc|sa)ss$/,
|
|
10
12
|
type: useModules ? "css/auto" : "css",
|
|
11
13
|
use: baseLoaders,
|
|
14
|
+
parser: cssParser,
|
|
12
15
|
},
|
|
13
16
|
server: {
|
|
14
17
|
test: /\.(c|sc|sa)ss$/,
|
|
15
18
|
type: useModules ? "css/auto" : "css",
|
|
16
|
-
generator: {
|
|
17
|
-
css: {
|
|
18
|
-
exportOnlyLocals: true,
|
|
19
|
-
},
|
|
20
|
-
},
|
|
19
|
+
generator: { css: { exportOnlyLocals: true } },
|
|
21
20
|
use: baseLoaders,
|
|
21
|
+
parser: cssParser,
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
24
|
}
|
|
@@ -40,3 +40,35 @@ export function createBabelRule({ isServer, isDev, plugins = [], }) {
|
|
|
40
40
|
],
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
export function createAssetRules(isDev) {
|
|
44
|
+
const filename = isDev ? "assets/[name][ext]" : "assets/[name].[contenthash:8][ext]";
|
|
45
|
+
const sharedRawRule = {
|
|
46
|
+
test: /\.(png|jpe?g|gif|webp|avif|ico|svg|json)$/i,
|
|
47
|
+
resourceQuery: /raw/,
|
|
48
|
+
type: "asset/source",
|
|
49
|
+
};
|
|
50
|
+
const sharedUrlRule = {
|
|
51
|
+
test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i,
|
|
52
|
+
resourceQuery: /url/,
|
|
53
|
+
type: "asset/resource",
|
|
54
|
+
generator: { filename },
|
|
55
|
+
};
|
|
56
|
+
const sharedInlineRule = {
|
|
57
|
+
test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i,
|
|
58
|
+
resourceQuery: /inline/,
|
|
59
|
+
type: "asset/inline",
|
|
60
|
+
};
|
|
61
|
+
const sharedAssetRules = [sharedRawRule, sharedUrlRule, sharedInlineRule];
|
|
62
|
+
const clientRules = [
|
|
63
|
+
...sharedAssetRules,
|
|
64
|
+
{ test: /\.(png|jpe?g|gif|webp|avif|ico)$/i, type: "asset/resource", generator: { filename } },
|
|
65
|
+
{ test: /\.svg$/i, type: "asset", parser: { dataUrlCondition: { maxSize: 8192 } }, generator: { filename } },
|
|
66
|
+
{ test: /\.json$/i, type: "json" },
|
|
67
|
+
];
|
|
68
|
+
const serverRules = [
|
|
69
|
+
...sharedAssetRules,
|
|
70
|
+
{ test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i, type: "asset/source" },
|
|
71
|
+
{ test: /\.json$/i, type: "json" },
|
|
72
|
+
];
|
|
73
|
+
return { client: clientRules, server: serverRules };
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anaemia/bundler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"main": "./dist/index.js",
|
|
5
6
|
"types": "./dist/index.d.ts",
|
|
6
|
-
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./analyzer": {
|
|
13
|
+
"types": "./dist/analyzer/index.d.ts",
|
|
14
|
+
"default": "./dist/analyzer/index.js"
|
|
11
15
|
}
|
|
12
16
|
},
|
|
13
17
|
"dependencies": {
|
|
14
|
-
"@anaemia/core": "^0.
|
|
18
|
+
"@anaemia/core": "^0.5.0",
|
|
15
19
|
"@babel/core": "^7.29.7",
|
|
16
20
|
"@babel/preset-typescript": "^7.29.7",
|
|
17
21
|
"@rspack/core": "^2.0.5",
|
|
18
22
|
"babel-loader": "^10.1.1",
|
|
19
23
|
"babel-preset-solid": "^1.9.12",
|
|
24
|
+
"dotenv": "^17.4.2",
|
|
25
|
+
"dotenv-expand": "^13.0.0",
|
|
20
26
|
"glob": "^13.0.6",
|
|
21
27
|
"jiti": "^2.7.0",
|
|
28
|
+
"oxc-parser": "^0.133.0",
|
|
29
|
+
"picocolors": "^1.1.1",
|
|
22
30
|
"sass": "^1.100.0",
|
|
23
31
|
"sass-loader": "^17.0.0",
|
|
24
32
|
"solid-refresh": "^0.7.8",
|
package/src/aliases.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import path from "path";
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
export function getAliases(appRoot: string) {
|
|
4
4
|
return {
|
|
@@ -8,4 +8,4 @@ export function getAliases(appRoot: string) {
|
|
|
8
8
|
"@features": path.resolve(appRoot, "./src/features"),
|
|
9
9
|
"@routes": path.resolve(appRoot, "./src/routes"),
|
|
10
10
|
};
|
|
11
|
-
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AstNode } from "./ast-walker.js";
|
|
2
|
+
|
|
3
|
+
export function prop<T = unknown>(node: AstNode, key: string): T {
|
|
4
|
+
return (node as Record<string, unknown>)[key] as T;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function child(node: AstNode, key: string): AstNode | null {
|
|
8
|
+
const value = (node as Record<string, unknown>)[key];
|
|
9
|
+
if (value && typeof value === "object" && typeof (value as AstNode).type === "string") {
|
|
10
|
+
return value as AstNode;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function children(node: AstNode, key: string): AstNode[] {
|
|
16
|
+
const value = (node as Record<string, unknown>)[key];
|
|
17
|
+
if (!Array.isArray(value)) return [];
|
|
18
|
+
return value.filter(
|
|
19
|
+
(v): v is AstNode =>
|
|
20
|
+
v !== null && v !== undefined && typeof v === "object" && typeof (v as Record<string, unknown>).type === "string",
|
|
21
|
+
);
|
|
22
|
+
}
|