@schalkneethling/miyagi-core 4.0.2 → 4.1.1
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/lib/build/index.js +16 -0
- package/lib/cli/drupal-assets.js +159 -0
- package/lib/cli/index.js +2 -0
- package/lib/default-config.js +5 -0
- package/lib/drupal/load-assets-config.js +94 -0
- package/lib/drupal/resolve-library-assets.js +189 -0
- package/lib/index.js +27 -4
- package/lib/init/args.js +38 -0
- package/lib/init/config.js +17 -0
- package/lib/init/router.js +1 -0
- package/lib/init/static.js +30 -0
- package/lib/init/watcher.js +21 -0
- package/lib/mocks/get.js +4 -0
- package/lib/render/helpers/resolve-assets.js +58 -0
- package/lib/render/views/iframe/component.js +9 -7
- package/lib/render/views/iframe/variation.standalone.js +9 -7
- package/package.json +10 -8
package/lib/build/index.js
CHANGED
|
@@ -380,6 +380,19 @@ export default () => {
|
|
|
380
380
|
});
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
const sharedCss = (assetsConfig.shared?.css || []).map((file) =>
|
|
384
|
+
path.join(
|
|
385
|
+
assetsConfig.root,
|
|
386
|
+
typeof file === "string" ? file : file.src,
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
const sharedJs = (assetsConfig.shared?.js || []).map((file) =>
|
|
390
|
+
path.join(
|
|
391
|
+
assetsConfig.root,
|
|
392
|
+
typeof file === "string" ? file : file.src,
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
|
|
383
396
|
const cssJsFiles = [
|
|
384
397
|
...new Set([
|
|
385
398
|
...assetsConfig.css.map((file) =>
|
|
@@ -394,6 +407,8 @@ export default () => {
|
|
|
394
407
|
typeof file === "string" ? file : file.src,
|
|
395
408
|
),
|
|
396
409
|
),
|
|
410
|
+
...sharedCss,
|
|
411
|
+
...sharedJs,
|
|
397
412
|
...assetsConfig.customProperties.files.map((file) =>
|
|
398
413
|
path.join(assetsConfig.root, file),
|
|
399
414
|
),
|
|
@@ -655,6 +670,7 @@ export default () => {
|
|
|
655
670
|
res: global.app,
|
|
656
671
|
component,
|
|
657
672
|
componentData: data.resolved,
|
|
673
|
+
componentDeclaredAssets: data.$assets || null,
|
|
658
674
|
cb: async (err, response) => {
|
|
659
675
|
if (err) {
|
|
660
676
|
if (typeof err === "string") {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
import log from "../logger.js";
|
|
7
|
+
import { loadAssetsConfig } from "../drupal/load-assets-config.js";
|
|
8
|
+
import {
|
|
9
|
+
parseLibrariesYaml,
|
|
10
|
+
resolveComponentAssets,
|
|
11
|
+
mapLibraryToComponent,
|
|
12
|
+
} from "../drupal/resolve-library-assets.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{src: string, type?: string}} JsEntry
|
|
16
|
+
* @typedef {{css: string[], js: JsEntry[]}} ComponentAssets
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} args - CLI arguments from yargs
|
|
21
|
+
*/
|
|
22
|
+
export default async function drupalAssets(args) {
|
|
23
|
+
let config;
|
|
24
|
+
try {
|
|
25
|
+
config = await loadAssetsConfig(args);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
log("error", /** @type {Error} */ (err).message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!config.libraries) {
|
|
32
|
+
log("error", "No libraries file specified. Use --libraries or configure it in .miyagi-assets.js.");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let yamlContent;
|
|
37
|
+
try {
|
|
38
|
+
yamlContent = await readFile(config.libraries, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
log("error", `Could not read libraries file: ${config.libraries}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const librariesMap = parseLibrariesYaml(yamlContent);
|
|
45
|
+
const targetLibraries = config.components || Object.keys(librariesMap);
|
|
46
|
+
const componentsFolder = global.config?.components?.folder || "src";
|
|
47
|
+
|
|
48
|
+
let updatedCount = 0;
|
|
49
|
+
|
|
50
|
+
for (const libraryName of targetLibraries) {
|
|
51
|
+
if (!librariesMap[libraryName]) {
|
|
52
|
+
log("warn", `Library "${libraryName}" not found in ${config.libraries} — skipping.`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const componentPath = mapLibraryToComponent(
|
|
57
|
+
libraryName,
|
|
58
|
+
config.mapping,
|
|
59
|
+
componentsFolder,
|
|
60
|
+
config.autoDiscoveryPrefixes ?? undefined,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!componentPath) {
|
|
64
|
+
log("warn", `Could not map library "${libraryName}" to a component folder — skipping.`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const assets = resolveComponentAssets(
|
|
69
|
+
libraryName,
|
|
70
|
+
librariesMap,
|
|
71
|
+
config.ignorePrefixes,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (config.dryRun) {
|
|
75
|
+
log("info", `[dry-run] ${libraryName} → ${componentPath}`);
|
|
76
|
+
console.log(JSON.stringify({ $assets: assets }, null, "\t"));
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const updated = await updateMockFile(
|
|
81
|
+
path.join(componentsFolder, componentPath),
|
|
82
|
+
assets,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (updated) {
|
|
86
|
+
updatedCount++;
|
|
87
|
+
log("info", `Updated $assets in ${componentPath}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!config.dryRun) {
|
|
92
|
+
log("success", `Done. Updated ${updatedCount} component(s).`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reads a component's mock file, injects/replaces $assets, writes back.
|
|
98
|
+
* @param {string} componentDir - absolute or relative path to component folder
|
|
99
|
+
* @param {ComponentAssets} assets
|
|
100
|
+
* @returns {Promise<boolean>} true if file was updated
|
|
101
|
+
*/
|
|
102
|
+
async function updateMockFile(componentDir, assets) {
|
|
103
|
+
const mocksConfig = global.config?.files?.mocks || {
|
|
104
|
+
name: "mocks",
|
|
105
|
+
extension: ["yaml", "yml", "json", "js"],
|
|
106
|
+
};
|
|
107
|
+
const extensions = Array.isArray(mocksConfig.extension)
|
|
108
|
+
? mocksConfig.extension
|
|
109
|
+
: [mocksConfig.extension];
|
|
110
|
+
|
|
111
|
+
for (const ext of extensions) {
|
|
112
|
+
const filePath = path.join(componentDir, `${mocksConfig.name}.${ext}`);
|
|
113
|
+
|
|
114
|
+
let content;
|
|
115
|
+
try {
|
|
116
|
+
content = await readFile(filePath, "utf8");
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (["yaml", "yml"].includes(ext)) {
|
|
122
|
+
/** @type {Record<string, unknown>} */
|
|
123
|
+
const data = /** @type {Record<string, unknown>} */ (
|
|
124
|
+
yaml.load(content) || {}
|
|
125
|
+
);
|
|
126
|
+
data.$assets = cleanAssets(assets);
|
|
127
|
+
await writeFile(filePath, yaml.dump(data, { indent: 2 }));
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ext === "json") {
|
|
132
|
+
/** @type {Record<string, unknown>} */
|
|
133
|
+
const data = JSON.parse(content);
|
|
134
|
+
data.$assets = cleanAssets(assets);
|
|
135
|
+
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Strips empty arrays from assets to keep mock files clean.
|
|
145
|
+
* @param {ComponentAssets} assets
|
|
146
|
+
* @returns {Partial<ComponentAssets>}
|
|
147
|
+
*/
|
|
148
|
+
function cleanAssets(assets) {
|
|
149
|
+
/** @type {Partial<ComponentAssets>} */
|
|
150
|
+
const result = {};
|
|
151
|
+
if (assets.css?.length > 0) {
|
|
152
|
+
result.css = assets.css;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (assets.js?.length > 0) {
|
|
156
|
+
result.js = assets.js;
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
package/lib/cli/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import lintImport from "./lint.js";
|
|
2
2
|
import componentImport from "./component.js";
|
|
3
|
+
import drupalAssetsImport from "./drupal-assets.js";
|
|
3
4
|
|
|
4
5
|
export const lint = lintImport;
|
|
5
6
|
export const component = componentImport;
|
|
7
|
+
export const drupalAssets = drupalAssetsImport;
|
package/lib/default-config.js
CHANGED
|
@@ -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/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 {
|
|
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")
|
package/lib/init/config.js
CHANGED
|
@@ -216,6 +216,23 @@ export default (userConfig = {}) => {
|
|
|
216
216
|
);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
if (config.assets.shared) {
|
|
220
|
+
if (config.assets.shared.css) {
|
|
221
|
+
config.assets.shared.css = getCssFilesArray(
|
|
222
|
+
config.assets.shared.css,
|
|
223
|
+
manifest,
|
|
224
|
+
config.assets.root,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (config.assets.shared.js) {
|
|
228
|
+
config.assets.shared.js = getJsFilesArray(
|
|
229
|
+
config.assets.shared.js,
|
|
230
|
+
manifest,
|
|
231
|
+
config.assets.root,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
219
236
|
if (!config.assets.customProperties) {
|
|
220
237
|
config.assets.customProperties = {};
|
|
221
238
|
}
|
package/lib/init/router.js
CHANGED
package/lib/init/static.js
CHANGED
|
@@ -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
|
}
|
package/lib/init/watcher.js
CHANGED
|
@@ -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/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
|
+
}
|
|
@@ -6,6 +6,7 @@ import * as helpers from "../../../helpers.js";
|
|
|
6
6
|
import validateMocks from "../../../validator/mocks.js";
|
|
7
7
|
import { getComponentData } from "../../../mocks/index.js";
|
|
8
8
|
import { getUserUiConfig, getThemeMode } from "../../helpers.js";
|
|
9
|
+
import resolveAssets from "../../helpers/resolve-assets.js";
|
|
9
10
|
import log from "../../../logger.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -216,18 +217,19 @@ async function renderVariations({
|
|
|
216
217
|
({ shortPath }) => shortPath === component.paths.dir.short,
|
|
217
218
|
);
|
|
218
219
|
|
|
220
|
+
const componentDeclaredAssets =
|
|
221
|
+
context.length > 0 ? context[0].$assets || null : null;
|
|
222
|
+
const { cssFiles, jsFilesHead, jsFilesBody } =
|
|
223
|
+
resolveAssets(componentDeclaredAssets);
|
|
224
|
+
|
|
219
225
|
await res.render(
|
|
220
226
|
"iframe_component.twig.miyagi",
|
|
221
227
|
{
|
|
222
228
|
lang: global.config.ui.lang,
|
|
223
229
|
variations,
|
|
224
|
-
cssFiles
|
|
225
|
-
jsFilesHead
|
|
226
|
-
|
|
227
|
-
),
|
|
228
|
-
jsFilesBody: global.config.assets.js.filter(
|
|
229
|
-
(entry) => entry.position === "body",
|
|
230
|
-
),
|
|
230
|
+
cssFiles,
|
|
231
|
+
jsFilesHead,
|
|
232
|
+
jsFilesBody,
|
|
231
233
|
assets: {
|
|
232
234
|
css: componentsEntry
|
|
233
235
|
? componentsEntry.assets.css
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import config from "../../../default-config.js";
|
|
3
3
|
import { getUserUiConfig } from "../../helpers.js";
|
|
4
|
+
import resolveAssets from "../../helpers/resolve-assets.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {object} object - parameter object
|
|
7
8
|
* @param {object} [object.res] - the express response object
|
|
8
9
|
* @param {object} object.component
|
|
9
10
|
* @param {object} object.componentData
|
|
11
|
+
* @param {object|null} [object.componentDeclaredAssets] - $assets from mocks
|
|
10
12
|
* @param {Function} [object.cb] - callback function
|
|
11
13
|
* @param {object} [object.cookies]
|
|
12
14
|
* @returns {Promise} gets resolved when the variation has been rendered
|
|
@@ -15,6 +17,7 @@ export default async function renderIframeVariationStandalone({
|
|
|
15
17
|
res,
|
|
16
18
|
component,
|
|
17
19
|
componentData,
|
|
20
|
+
componentDeclaredAssets = null,
|
|
18
21
|
cb,
|
|
19
22
|
cookies,
|
|
20
23
|
}) {
|
|
@@ -38,17 +41,16 @@ export default async function renderIframeVariationStandalone({
|
|
|
38
41
|
({ shortPath }) => shortPath === directoryPath,
|
|
39
42
|
);
|
|
40
43
|
|
|
44
|
+
const { cssFiles, jsFilesHead, jsFilesBody } =
|
|
45
|
+
resolveAssets(componentDeclaredAssets);
|
|
46
|
+
|
|
41
47
|
await res.render(
|
|
42
48
|
"component_variation.twig.miyagi",
|
|
43
49
|
{
|
|
44
50
|
html: result,
|
|
45
|
-
cssFiles
|
|
46
|
-
jsFilesHead
|
|
47
|
-
|
|
48
|
-
),
|
|
49
|
-
jsFilesBody: global.config.assets.js.filter(
|
|
50
|
-
(entry) => entry.position === "body",
|
|
51
|
-
),
|
|
51
|
+
cssFiles,
|
|
52
|
+
jsFilesHead,
|
|
53
|
+
jsFilesBody,
|
|
52
54
|
assets: {
|
|
53
55
|
css: componentsEntry
|
|
54
56
|
? componentsEntry.assets.css
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schalkneethling/miyagi-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "miyagi is a component development tool for JavaScript template engines.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Schalk Neethling <schalkneethling@duck.com>, Michael Großklaus <mail@mgrossklaus.de> (https://www.mgrossklaus.de)",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"bugs": "https://github.com/miyagi-dev/miyagi/issues",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "
|
|
12
|
+
"url": "https://github.com/schalkneethling/miyagi.git"
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
15
|
"keywords": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"frontend"
|
|
21
21
|
],
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=24"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"api",
|
|
@@ -43,9 +43,10 @@
|
|
|
43
43
|
"directory-tree": "^3.5.2",
|
|
44
44
|
"express": "^5.1.0",
|
|
45
45
|
"js-yaml": "^4.1.0",
|
|
46
|
-
"marked": "^
|
|
46
|
+
"marked": "^17.0.2",
|
|
47
47
|
"node-watch": "^0.7.4",
|
|
48
|
-
"twing": "7.
|
|
48
|
+
"twing": "7.3.1",
|
|
49
|
+
"valibot": "^1.2.0",
|
|
49
50
|
"ws": "^8.18.3",
|
|
50
51
|
"yargs": "^18.0.0"
|
|
51
52
|
},
|
|
@@ -53,11 +54,13 @@
|
|
|
53
54
|
"@eslint/js": "^9.39.2",
|
|
54
55
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
55
56
|
"@rollup/plugin-terser": "^0.4.4",
|
|
57
|
+
"@types/js-yaml": "^4.0.9",
|
|
56
58
|
"@types/node": "^24.10.0",
|
|
59
|
+
"@types/yargs": "^17.0.35",
|
|
57
60
|
"@vitest/coverage-v8": "^4.0.6",
|
|
58
61
|
"cssnano": "^7.1.2",
|
|
59
62
|
"eslint": "^9.39.0",
|
|
60
|
-
"eslint-plugin-jsdoc": "^
|
|
63
|
+
"eslint-plugin-jsdoc": "^62.5.4",
|
|
61
64
|
"globals": "^15.15.0",
|
|
62
65
|
"gulp": "^5.0.1",
|
|
63
66
|
"gulp-postcss": "^10.0.0",
|
|
@@ -75,8 +78,7 @@
|
|
|
75
78
|
"build": "gulp build",
|
|
76
79
|
"test": "vitest run --coverage --coverage.include=api --coverage.include=lib",
|
|
77
80
|
"lint": "stylelint frontend/assets/css/ && eslint lib/ && eslint frontend/assets/js/",
|
|
78
|
-
"fix": "eslint lib/ --fix && eslint frontend/assets/js/ --fix"
|
|
79
|
-
"release": "standard-version"
|
|
81
|
+
"fix": "eslint lib/ --fix && eslint frontend/assets/js/ --fix"
|
|
80
82
|
},
|
|
81
83
|
"browserslist": [
|
|
82
84
|
"last 2 versions",
|