@schalkneethling/miyagi-core 4.0.2 → 4.2.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.
@@ -0,0 +1,94 @@
1
+ import path from "node:path";
2
+ import * as v from "valibot";
3
+
4
+ const DrupalBlockSchema = v.object({
5
+ libraries: v.string(),
6
+ ignorePrefixes: v.optional(v.array(v.string())),
7
+ mapping: v.optional(v.record(v.string(), v.string())),
8
+ autoDiscoveryPrefixes: v.optional(v.nullable(v.array(v.string()))),
9
+ components: v.optional(v.nullable(v.array(v.string()))),
10
+ });
11
+
12
+ const ConfigSchema = v.object({
13
+ engine: v.picklist(["drupal"]),
14
+ drupal: DrupalBlockSchema,
15
+ });
16
+
17
+ export const NormalizedConfigSchema = v.object({
18
+ engine: v.string(),
19
+ libraries: v.optional(v.string()),
20
+ ignorePrefixes: v.array(v.string()),
21
+ mapping: v.record(v.string(), v.string()),
22
+ autoDiscoveryPrefixes: v.nullable(v.array(v.string())),
23
+ components: v.nullable(v.array(v.string())),
24
+ dryRun: v.boolean(),
25
+ });
26
+
27
+ /** @typedef {v.InferOutput<typeof NormalizedConfigSchema>} NormalizedConfig */
28
+
29
+ /**
30
+ * Loads the .miyagi-assets.js config and merges CLI overrides.
31
+ * @param {object} cliArgs
32
+ * @param {string} [cliArgs.engine] - engine to use (default: "drupal")
33
+ * @param {string} [cliArgs.config] - path to config file
34
+ * @param {string} [cliArgs.libraries] - CLI override for libraries path
35
+ * @param {string[]} [cliArgs.components] - CLI override for components list
36
+ * @param {string[]} [cliArgs.ignorePrefixes] - CLI override for ignore prefixes
37
+ * @param {boolean} [cliArgs.dryRun] - dry-run mode
38
+ * @returns {Promise<NormalizedConfig>} normalized config
39
+ */
40
+ export async function loadAssetsConfig(cliArgs) {
41
+ let fileConfig = null;
42
+
43
+ const configPath = cliArgs.config || ".miyagi-assets.js";
44
+
45
+ try {
46
+ const resolved = path.resolve(configPath);
47
+ const mod = await import(resolved);
48
+ fileConfig = mod.default || mod;
49
+ } catch {
50
+ if (!cliArgs.libraries) {
51
+ throw new Error(
52
+ `Could not load config file "${configPath}" and no --libraries flag provided.`,
53
+ );
54
+ }
55
+ }
56
+
57
+ if (fileConfig) {
58
+ const result = v.safeParse(ConfigSchema, fileConfig);
59
+
60
+ if (!result.success) {
61
+ const messages = result.issues.map((issue) => {
62
+ const issuePath =
63
+ issue.path?.map((path) => path.key).join(".") || "root";
64
+ return `${issuePath}: ${issue.message}`;
65
+ });
66
+ throw new Error(`Invalid config: ${messages.join("; ")}`);
67
+ }
68
+
69
+ const { engine } = result.output;
70
+ const engineBlock = result.output[engine];
71
+
72
+ return {
73
+ engine,
74
+ libraries: cliArgs.libraries || engineBlock.libraries,
75
+ ignorePrefixes:
76
+ cliArgs.ignorePrefixes || engineBlock.ignorePrefixes || [],
77
+ mapping: engineBlock.mapping || {},
78
+ autoDiscoveryPrefixes: engineBlock.autoDiscoveryPrefixes ?? null,
79
+ components:
80
+ cliArgs.components || engineBlock.components || null,
81
+ dryRun: cliArgs.dryRun || false,
82
+ };
83
+ }
84
+
85
+ return {
86
+ engine: cliArgs.engine || "drupal",
87
+ libraries: cliArgs.libraries,
88
+ ignorePrefixes: cliArgs.ignorePrefixes || [],
89
+ mapping: {},
90
+ autoDiscoveryPrefixes: null,
91
+ components: cliArgs.components || null,
92
+ dryRun: cliArgs.dryRun || false,
93
+ };
94
+ }
@@ -0,0 +1,189 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import yaml from "js-yaml";
6
+
7
+ /**
8
+ * @typedef {{src: string, type?: string}} JsEntry
9
+ * @typedef {{prefix: string, name: string}} DepEntry
10
+ * @typedef {{css: string[], js: JsEntry[], dependencies: DepEntry[]}} LibraryEntry
11
+ */
12
+
13
+ /**
14
+ * Parses a Drupal *.libraries.yml string into a normalized map.
15
+ * @param {string} yamlContent - raw YAML string (not a file path)
16
+ * @returns {Record<string, LibraryEntry>}
17
+ */
18
+ export function parseLibrariesYaml(yamlContent) {
19
+ /**
20
+ * @type {Record<string, {
21
+ * css?: Record<string, Record<string, object>>,
22
+ * js?: Record<string, {attributes?: {type?: string}}>,
23
+ * dependencies?: string[]
24
+ * }>}
25
+ */
26
+ const raw = /** @type {never} */ (yaml.load(yamlContent));
27
+ /** @type {Record<string, LibraryEntry>} */
28
+ const map = {};
29
+
30
+ for (const [name, entry] of Object.entries(raw)) {
31
+ const css = entry.css
32
+ ? Object.values(entry.css).flatMap(Object.keys)
33
+ : [];
34
+
35
+ const js = entry.js
36
+ ? Object.entries(entry.js).map(([src, opts]) =>
37
+ opts?.attributes?.type
38
+ ? { src, type: opts.attributes.type }
39
+ : { src },
40
+ )
41
+ : [];
42
+
43
+ const dependencies = (entry.dependencies || [])
44
+ .filter((dep) => dep.includes("/"))
45
+ .map((dep) => ({
46
+ prefix: dep.slice(0, dep.indexOf("/")),
47
+ name: dep.slice(dep.indexOf("/") + 1),
48
+ }));
49
+
50
+ map[name] = { css, js, dependencies };
51
+ }
52
+
53
+ return map;
54
+ }
55
+
56
+ /**
57
+ * Recursively resolves all CSS and JS assets for a library, depth-first.
58
+ * Dependencies are collected before the component's own assets to preserve
59
+ * the CSS cascade (base styles before component styles) and ensure JS
60
+ * dependencies are available before scripts that rely on them.
61
+ * @param {string} libraryName
62
+ * @param {Record<string, LibraryEntry>} librariesMap - output of parseLibrariesYaml
63
+ * @param {string[]} ignorePrefixes - dependency prefixes to skip
64
+ * @returns {{css: string[], js: JsEntry[]}}
65
+ */
66
+ export function resolveComponentAssets(
67
+ libraryName,
68
+ librariesMap,
69
+ ignorePrefixes,
70
+ ) {
71
+ const cssSet = [];
72
+ const jsSet = [];
73
+ const resolved = new Set();
74
+
75
+ /**
76
+ * @param {string} name
77
+ * @param {Set<string>} ancestors
78
+ */
79
+ function walk(name, ancestors) {
80
+ if (ancestors.has(name)) {
81
+ console.warn(
82
+ `Circular dependency detected: "${name}" already in chain.`,
83
+ );
84
+ return;
85
+ }
86
+
87
+ if (resolved.has(name)) {
88
+ return;
89
+ }
90
+
91
+ const lib = librariesMap[name];
92
+ if (!lib) {
93
+ console.warn(
94
+ `Dependency "${name}" not found in libraries — skipping.`,
95
+ );
96
+ return;
97
+ }
98
+
99
+ const chain = new Set(ancestors);
100
+ chain.add(name);
101
+
102
+ for (const dep of lib.dependencies) {
103
+ if (ignorePrefixes.includes(dep.prefix)) {
104
+ continue;
105
+ }
106
+ walk(dep.name, chain);
107
+ }
108
+
109
+ for (const cssFile of lib.css) {
110
+ if (!cssSet.includes(cssFile)) {
111
+ cssSet.push(cssFile);
112
+ }
113
+ }
114
+
115
+ for (const jsEntry of lib.js) {
116
+ if (!jsSet.some((entry) => entry.src === jsEntry.src)) {
117
+ jsSet.push(jsEntry);
118
+ }
119
+ }
120
+
121
+ resolved.add(name);
122
+ }
123
+
124
+ walk(libraryName, new Set());
125
+ return { css: cssSet, js: jsSet };
126
+ }
127
+
128
+ const DEFAULT_PREFIXES = ["element-", "pattern-", "template-", "component-"];
129
+
130
+ /**
131
+ * Maps a Drupal library name to a miyagi component folder path.
132
+ * Uses explicit mapping first, falls back to auto-discovery.
133
+ * @param {string} libraryName
134
+ * @param {Record<string, string>} mapping - explicit library-to-folder mapping
135
+ * @param {string} componentsFolder - root components folder path
136
+ * @param {string[]} [autoDiscoveryPrefixes] - prefixes to strip when matching folder names
137
+ * @returns {string|null} component folder relative path, or null if not found
138
+ */
139
+ export function mapLibraryToComponent(
140
+ libraryName,
141
+ mapping,
142
+ componentsFolder,
143
+ autoDiscoveryPrefixes = DEFAULT_PREFIXES,
144
+ ) {
145
+ if (mapping[libraryName]) {
146
+ return mapping[libraryName];
147
+ }
148
+
149
+ const candidates = [libraryName];
150
+
151
+ for (const prefix of autoDiscoveryPrefixes) {
152
+ if (libraryName.startsWith(prefix)) {
153
+ candidates.push(libraryName.slice(prefix.length));
154
+ }
155
+ }
156
+
157
+ try {
158
+ /** @type {string[]} */
159
+ const searchDirs = [componentsFolder];
160
+ while (searchDirs.length > 0) {
161
+ const dir = /** @type {string} */ (searchDirs.pop());
162
+
163
+ if (!fs.existsSync(dir)) {
164
+ continue;
165
+ }
166
+
167
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
168
+
169
+ for (const entry of entries) {
170
+ if (!entry.isDirectory()) {
171
+ continue;
172
+ }
173
+
174
+ if (candidates.includes(entry.name)) {
175
+ return path.relative(
176
+ componentsFolder,
177
+ path.join(dir, entry.name),
178
+ );
179
+ }
180
+ searchDirs.push(path.join(dir, entry.name));
181
+ }
182
+ }
183
+ } catch {
184
+ // Swallow filesystem errors (missing/unreadable dirs) intentionally.
185
+ // Caller handles null return with a user-facing warning.
186
+ }
187
+
188
+ return null;
189
+ }
package/lib/i18n/en.js CHANGED
@@ -70,6 +70,10 @@ export default {
70
70
  invalid: "Mock data does not match schema file.",
71
71
  noSchemaFound:
72
72
  "No schema file found or the schema file could not be parsed as valid JSON.",
73
+ schemaMissing:
74
+ "Component {{component}} has no schema file (expected: {{schemaFile}}). Consider adding it to components.ignores if this is expected.",
75
+ schemaParseFailed:
76
+ "Schema file {{schemaFile}} could not be parsed as {{format}}.",
73
77
  },
74
78
  },
75
79
  serverStarted: "Running miyagi server at http://localhost:{{port}}",
package/lib/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  /**
2
4
  * The miyagi module
3
5
  * @module index
@@ -9,7 +11,11 @@ import log from "./logger.js";
9
11
  import yargs from "./init/args.js";
10
12
  import mockGenerator from "./generator/mocks.js";
11
13
  import getConfig from "./config.js";
12
- import { lint, component as createComponentViaCli } from "./cli/index.js";
14
+ import {
15
+ lint,
16
+ component as createComponentViaCli,
17
+ drupalAssets,
18
+ } from "./cli/index.js";
13
19
  import apiApp from "../api/app.js";
14
20
 
15
21
  /**
@@ -57,6 +63,14 @@ function argsIncludeLint(args) {
57
63
  return args._.includes("lint");
58
64
  }
59
65
 
66
+ /**
67
+ * @param {object} args
68
+ * @returns {boolean}
69
+ */
70
+ function argsIncludeDrupalAssets(args) {
71
+ return args._.includes("drupal-assets");
72
+ }
73
+
60
74
  /**
61
75
  * Runs the mock generator
62
76
  * @param {object} config - the user configuration object
@@ -97,26 +111,35 @@ export default async function Miyagi(cmd, { isBuild: isApiBuild } = {}) {
97
111
  let isComponentGenerator;
98
112
  let isMockGenerator;
99
113
  let isLinter;
114
+ let isDrupalAssets;
100
115
 
101
116
  if (cmd) {
102
117
  isBuild = cmd === "build";
103
118
  } else {
104
- args = yargs.argv;
119
+ args = await yargs.argv;
105
120
  isServer = argsIncludeServer(args);
106
121
  isBuild = argsIncludeBuild(args);
107
122
  isComponentGenerator = argsIncludeComponentGenerator(args);
108
123
  isMockGenerator = argsIncludeMockGenerator(args);
109
124
  isLinter = argsIncludeLint(args);
125
+ isDrupalAssets = argsIncludeDrupalAssets(args);
110
126
  }
111
127
 
112
- if (args.verbose) {
113
- process.env.VERBOSE = args.verbose;
128
+ if (args && args.verbose) {
129
+ process.env.VERBOSE = String(args.verbose);
114
130
  }
115
131
 
116
132
  if (isLinter) {
117
133
  return lint(args);
118
134
  }
119
135
 
136
+ if (isDrupalAssets) {
137
+ process.env.NODE_ENV = "development";
138
+ global.config = await getConfig(args);
139
+ await drupalAssets(args);
140
+ process.exit();
141
+ }
142
+
120
143
  if (isBuild || isComponentGenerator || isServer || isMockGenerator) {
121
144
  if (isBuild) {
122
145
  process.env.NODE_ENV = "production";
package/lib/init/args.js CHANGED
@@ -45,6 +45,44 @@ export default yargs(hideBin(process.argv))
45
45
  "lint",
46
46
  "Validates if the component's mock data matches its JSON schema",
47
47
  )
48
+ .command(
49
+ "drupal-assets",
50
+ "Resolves Drupal *.libraries.yml dependencies and updates component $assets in mock files",
51
+ {
52
+ engine: {
53
+ alias: "e",
54
+ description: "Engine to use for asset resolution",
55
+ type: "string",
56
+ choices: ["drupal"],
57
+ default: "drupal",
58
+ },
59
+ config: {
60
+ description: "Path to .miyagi-assets.js config file",
61
+ type: "string",
62
+ default: ".miyagi-assets.js",
63
+ },
64
+ libraries: {
65
+ alias: "l",
66
+ description: "Path to *.libraries.yml (overrides config)",
67
+ type: "string",
68
+ },
69
+ components: {
70
+ alias: "c",
71
+ description: "Library names to process (space-separated)",
72
+ type: "array",
73
+ },
74
+ "ignore-prefixes": {
75
+ description:
76
+ 'Dependency prefixes to skip (e.g. "core" to ignore core/jquery)',
77
+ type: "array",
78
+ },
79
+ "dry-run": {
80
+ description: "Print resolved $assets without writing files",
81
+ type: "boolean",
82
+ default: false,
83
+ },
84
+ },
85
+ )
48
86
  .help()
49
87
  .version(pkgJson.version)
50
88
  .alias("help", "h")
@@ -7,6 +7,7 @@ import deepMerge from "deepmerge";
7
7
  import log from "../logger.js";
8
8
  import appConfig from "../default-config.js";
9
9
  import { t, available as langAvailable } from "../i18n/index.js";
10
+ import { LINT_LOG_LEVELS } from "../constants/lint-log-levels.js";
10
11
  import fs from "fs";
11
12
  import path from "path";
12
13
 
@@ -216,6 +217,23 @@ export default (userConfig = {}) => {
216
217
  );
217
218
  }
218
219
 
220
+ if (config.assets.shared) {
221
+ if (config.assets.shared.css) {
222
+ config.assets.shared.css = getCssFilesArray(
223
+ config.assets.shared.css,
224
+ manifest,
225
+ config.assets.root,
226
+ );
227
+ }
228
+ if (config.assets.shared.js) {
229
+ config.assets.shared.js = getJsFilesArray(
230
+ config.assets.shared.js,
231
+ manifest,
232
+ config.assets.root,
233
+ );
234
+ }
235
+ }
236
+
219
237
  if (!config.assets.customProperties) {
220
238
  config.assets.customProperties = {};
221
239
  }
@@ -309,6 +327,14 @@ export default (userConfig = {}) => {
309
327
  merged.ui.lang = "en";
310
328
  }
311
329
 
330
+ if (!Object.values(LINT_LOG_LEVELS).includes(merged.lint.logLevel)) {
331
+ log(
332
+ "warn",
333
+ `Invalid config.lint.logLevel "${merged.lint.logLevel}". Falling back to "${defaultUserConfig.lint.logLevel}".`,
334
+ );
335
+ merged.lint.logLevel = defaultUserConfig.lint.logLevel;
336
+ }
337
+
312
338
  return merged;
313
339
  };
314
340
 
@@ -222,6 +222,7 @@ export default function Router() {
222
222
  res,
223
223
  component: routesEntry,
224
224
  componentData: data?.resolved ?? {},
225
+ componentDeclaredAssets: data?.$assets || null,
225
226
  cookies: req.cookies,
226
227
  });
227
228
  }
@@ -75,6 +75,35 @@ function registerUserFiles(files) {
75
75
  }
76
76
  }
77
77
 
78
+ /**
79
+ * Registers static routes for assets.shared CSS and JS files.
80
+ * @returns {void}
81
+ */
82
+ function registerSharedFiles() {
83
+ const { assets } = global.config;
84
+
85
+ if (!assets?.shared) return;
86
+
87
+ for (const file of assets.shared.css || []) {
88
+ if (file.startsWith("https://")) continue;
89
+
90
+ global.app.use(
91
+ path.join("/", path.dirname(file)),
92
+ express.static(path.join(assets.root, path.dirname(file))),
93
+ );
94
+ }
95
+
96
+ for (const file of assets.shared.js || []) {
97
+ const src = file.src || file;
98
+ if (src.startsWith("https://")) continue;
99
+
100
+ global.app.use(
101
+ path.join("/", path.dirname(src)),
102
+ express.static(path.join(assets.root, path.dirname(src))),
103
+ );
104
+ }
105
+ }
106
+
78
107
  /**
79
108
  * @returns {void}
80
109
  */
@@ -128,6 +157,7 @@ export default function initStatic() {
128
157
  registerUserComponentAssets();
129
158
  registerUserFiles("css");
130
159
  registerUserFiles("js");
160
+ registerSharedFiles();
131
161
  registerCustomPropertyFiles();
132
162
  registerAssetFolder();
133
163
  }
@@ -292,6 +292,25 @@ export default function Watcher(server) {
292
292
 
293
293
  const { components, docs, assets, extensions } = global.config;
294
294
 
295
+ const sharedCssToWatch = (assets.shared?.css || [])
296
+ .filter(
297
+ (f) =>
298
+ !f.startsWith("http://") &&
299
+ !f.startsWith("https://") &&
300
+ !f.startsWith("://"),
301
+ )
302
+ .map((f) => path.join(global.config.assets.root, f));
303
+
304
+ const sharedJsToWatch = (assets.shared?.js || [])
305
+ .map((file) => file.src || file)
306
+ .filter(
307
+ (f) =>
308
+ !f.startsWith("http://") &&
309
+ !f.startsWith("https://") &&
310
+ !f.startsWith("://"),
311
+ )
312
+ .map((f) => path.join(global.config.assets.root, f));
313
+
295
314
  const foldersToWatch = [
296
315
  ...assets.folder.map((f) => path.join(global.config.assets.root, f)),
297
316
  ...assets.css
@@ -311,6 +330,8 @@ export default function Watcher(server) {
311
330
  !f.startsWith("://"),
312
331
  )
313
332
  .map((f) => path.join(global.config.assets.root, f)),
333
+ ...sharedCssToWatch,
334
+ ...sharedJsToWatch,
314
335
  ];
315
336
 
316
337
  if (components.folder) {
package/lib/logger.js CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ LINT_LOG_LEVEL_ORDER,
3
+ LINT_LOG_LEVELS,
4
+ } from "./constants/lint-log-levels.js";
5
+
1
6
  const COLORS = {
2
7
  grey: "\x1b[90m",
3
8
  red: "\x1b[31m",
@@ -21,8 +26,17 @@ const TYPES = {
21
26
  * @param {string|Error} [verboseMessage]
22
27
  */
23
28
  export default function log(type, message, verboseMessage) {
24
- if (process.env.MIYAGI_JS_API) return;
25
- if (!(type in TYPES)) return;
29
+ if (process.env.MIYAGI_JS_API) {
30
+ return;
31
+ }
32
+
33
+ if (!(type in TYPES)) {
34
+ return;
35
+ }
36
+
37
+ if (!shouldLogType(type)) {
38
+ return;
39
+ }
26
40
 
27
41
  const date = new Date();
28
42
  const year = date.getFullYear();
@@ -59,6 +73,27 @@ export default function log(type, message, verboseMessage) {
59
73
  }
60
74
  }
61
75
 
76
+ /**
77
+ * @param {string} type
78
+ * @returns {boolean}
79
+ */
80
+ function shouldLogType(type) {
81
+ if (process.env.MIYAGI_LOG_CONTEXT !== "lint") {
82
+ return true;
83
+ }
84
+
85
+ const configuredLevel = process.env.MIYAGI_LOG_LEVEL || "error";
86
+ const normalizedType = type === "success" ? "info" : type;
87
+ const configuredLevelValue =
88
+ LINT_LOG_LEVEL_ORDER[configuredLevel] ??
89
+ LINT_LOG_LEVEL_ORDER[LINT_LOG_LEVELS.ERROR];
90
+ const typeLevelValue =
91
+ LINT_LOG_LEVEL_ORDER[normalizedType] ??
92
+ LINT_LOG_LEVEL_ORDER[LINT_LOG_LEVELS.INFO];
93
+
94
+ return typeLevelValue <= configuredLevelValue;
95
+ }
96
+
62
97
  /**
63
98
  * @param {string} color
64
99
  * @param {string} str
package/lib/mocks/get.js CHANGED
@@ -22,6 +22,7 @@ export const getComponentData = async function getComponentData(component) {
22
22
 
23
23
  if (componentJson) {
24
24
  context = [];
25
+ const componentDeclaredAssets = componentJson.$assets || null;
25
26
  let componentData = helpers.removeInternalKeys(componentJson);
26
27
  const rootData = helpers.cloneDeep(componentData);
27
28
  const componentVariations = componentJson.$variants;
@@ -55,6 +56,7 @@ export const getComponentData = async function getComponentData(component) {
55
56
  resolved: resolved,
56
57
  raw: merged,
57
58
  name: variationJson.$name,
59
+ $assets: componentDeclaredAssets,
58
60
  };
59
61
  }
60
62
  }
@@ -67,6 +69,7 @@ export const getComponentData = async function getComponentData(component) {
67
69
  resolved: data.resolved,
68
70
  raw: data.merged,
69
71
  name: componentJson.$name || config.defaultVariationName,
72
+ $assets: componentDeclaredAssets,
70
73
  });
71
74
  }
72
75
  }
@@ -85,6 +88,7 @@ export const getComponentData = async function getComponentData(component) {
85
88
  resolved: componentJson.$hidden ? {} : resolved,
86
89
  raw: componentJson.$hidden ? {} : merged,
87
90
  name: componentJson.$name || config.defaultVariationName,
91
+ $assets: componentDeclaredAssets,
88
92
  });
89
93
  }
90
94
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Resolves the final CSS and JS asset lists for a component render.
3
+ *
4
+ * Resolution logic:
5
+ * - If componentAssets ($assets from mocks) is provided:
6
+ * shared + componentAssets
7
+ * - Else if isolateComponents is true:
8
+ * shared only
9
+ * - Else (legacy fallback):
10
+ * global.config.assets.css / .js
11
+ * @param {object|null|undefined} componentAssets - the $assets declaration from mocks, or null/undefined
12
+ * @returns {{ cssFiles: string[], jsFilesHead: object[], jsFilesBody: object[] }}
13
+ */
14
+ export default function resolveAssets(componentAssets) {
15
+ const { shared, isolateComponents, css, js } = global.config.assets;
16
+
17
+ if (componentAssets) {
18
+ const mergedCss = [
19
+ ...shared.css,
20
+ ...(componentAssets.css || []),
21
+ ];
22
+ const mergedJs = [
23
+ ...shared.js,
24
+ ...(componentAssets.js || []),
25
+ ];
26
+
27
+ return {
28
+ cssFiles: mergedCss,
29
+ jsFilesHead: mergedJs.filter(
30
+ (entry) => entry.position === "head" || !entry.position,
31
+ ),
32
+ jsFilesBody: mergedJs.filter(
33
+ (entry) => entry.position === "body",
34
+ ),
35
+ };
36
+ }
37
+
38
+ if (isolateComponents) {
39
+ return {
40
+ cssFiles: [...shared.css],
41
+ jsFilesHead: shared.js.filter(
42
+ (entry) => entry.position === "head" || !entry.position,
43
+ ),
44
+ jsFilesBody: shared.js.filter(
45
+ (entry) => entry.position === "body",
46
+ ),
47
+ };
48
+ }
49
+
50
+ // Legacy fallback: return all global assets
51
+ return {
52
+ cssFiles: css,
53
+ jsFilesHead: js.filter(
54
+ (entry) => entry.position === "head" || !entry.position,
55
+ ),
56
+ jsFilesBody: js.filter((entry) => entry.position === "body"),
57
+ };
58
+ }