mage-obsidian 1.1.2 → 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/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +26 -25
- package/.oxlintrc.json +19 -0
- package/.prettierignore +7 -0
- package/.prettierrc.json +7 -0
- package/LICENSE +1 -1
- package/README.md +1 -0
- package/eslint.config.js +24 -0
- package/package.json +39 -13
- package/pnpm-workspace.yaml +4 -0
- package/src/cli/buildThemes.ts +263 -0
- package/src/config/default.ts +31 -0
- package/src/config/eslint.js +60 -0
- package/src/core/configResolver.ts +141 -0
- package/src/core/contractValidator.ts +56 -0
- package/src/core/cssResolver.ts +153 -0
- package/src/{service/generateInterceptors.js → core/generateInterceptors.ts} +79 -46
- package/src/core/generateJsconfig.ts +168 -0
- package/src/{service/moduleResolver.js → core/moduleResolver.ts} +79 -46
- package/src/core/preCompileFiles.ts +105 -0
- package/src/core/preCompileMagentoFiles.ts +34 -0
- package/src/core/themeResolverSync.ts +66 -0
- package/src/runtime/i18nCore.ts +113 -0
- package/src/runtime/interceptorManager.ts +186 -0
- package/src/runtime/islands.ts +84 -0
- package/src/runtime/sectionStoreCore.ts +259 -0
- package/src/utils/{findComponents.js → findComponents.ts} +29 -16
- package/src/utils/runWithConcurrency.ts +35 -0
- package/src/vite/defaultNodeResolver.ts +17 -0
- package/src/{service/inheritAssetsModuleResolver.js → vite/inheritAssetsModuleResolver.ts} +15 -11
- package/src/{service/inheritModuleResolver.js → vite/inheritModuleResolver.ts} +6 -10
- package/src/{service/interceptorsPlugin.js → vite/interceptorsPlugin.ts} +25 -19
- package/src/vite/magentoHrmRewrite.ts +44 -0
- package/src/vite/sharedPlugins.ts +54 -0
- package/src/vite/themeSourceWatcher.ts +96 -0
- package/src/vite/unresolvedModuleGuard.ts +87 -0
- package/tsconfig.json +34 -0
- package/vitest.config.js +18 -0
- package/jest.config.js +0 -5
- package/src/__tests__/__mocks__/configResolver.js +0 -35
- package/src/__tests__/fixtures/interceptors/emptyTarget.js +0 -2
- package/src/__tests__/fixtures/interceptors/pluginForNonFunction.js +0 -7
- package/src/__tests__/fixtures/interceptors/pluginModule.js +0 -13
- package/src/__tests__/fixtures/interceptors/targetModule.js +0 -12
- package/src/__tests__/fixtures/interceptors/targetNonFunction.js +0 -7
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/components/ComponentA.vue +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/components/ComponentB.vue +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/css/module.extend.css +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/js/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/module.config.js +0 -9
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/components/ComponentA.vue +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/components/ComponentB.vue +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/css/module.extend.css +0 -2
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/js/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/module.config.js +0 -7
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/components/ComponentA.vue +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/components/ComponentB.vue +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/css/module.extend.css +0 -2
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/js/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/module.config.js +0 -7
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/components/ComponentA.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/components/ComponentB.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/css/module.extend.css +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/js/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/module.config.js +0 -2
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/components/ComponentA.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/components/ComponentB.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/css/module.extend.css +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/js/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/module.config.js +0 -2
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/components/ComponentA.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/components/ComponentB.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/css/module.extend.css +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/js/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/module.config.js +0 -2
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/components/ComponentA.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/components/ComponentB.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/css/theme.source.css +0 -6
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/js/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/theme.config.js +0 -5
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-b/web/css/theme.source.css +0 -6
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-b/web/theme.config.js +0 -5
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/components/ComponentA.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/components/ComponentB.vue +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/css/module.extend.css +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/js/main.js +0 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/module.config.js +0 -2
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/web/css/theme.source.css +0 -6
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/web/theme.config.js +0 -2
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-d/web/css/theme.source.css +0 -6
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-d/web/theme.config.js +0 -3
- package/src/__tests__/magento_scenarios/app/etc/mage_obsidian_frontend_modules_a.json +0 -53
- package/src/__tests__/magento_scenarios/app/etc/mage_obsidian_frontend_modules_b.json +0 -50
- package/src/__tests__/service/generateInterceptors.test.js +0 -196
- package/src/__tests__/service/inheritModuleResolver.test.js +0 -61
- package/src/__tests__/service/interceptorManager_magento_behavior.test.js +0 -137
- package/src/__tests__/service/moduleResolver.test.js +0 -194
- package/src/__tests__/service/themeResolverSync.test.js +0 -97
- package/src/__tests__/vite-plugins/interceptorsPlugin.test.js +0 -121
- package/src/config/default.js +0 -27
- package/src/scripts/buildThemes.js +0 -132
- package/src/service/configResolver.js +0 -63
- package/src/service/cssResolver.js +0 -73
- package/src/service/defaultNodeResolver.js +0 -16
- package/src/service/interceptorManager.js +0 -137
- package/src/service/magentoHrmRewrite.js +0 -43
- package/src/service/preCompileFiles.js +0 -37
- package/src/service/preCompileMagentoFiles.js +0 -16
- package/src/service/setupGlobals.js +0 -19
- package/src/service/themeResolverSync.js +0 -67
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { DEPENDENCY_CONFIG_FILE_PATH, OUTPUT_THEME_DIR } from "../config/default.ts";
|
|
5
|
+
import { validateContract } from "./contractValidator.ts";
|
|
6
|
+
|
|
7
|
+
const REGENERATE_HINT =
|
|
8
|
+
"Try running `bin/magento mage-obsidian:frontend:config --generate` to generate the configuration file.";
|
|
9
|
+
|
|
10
|
+
// Minimal shapes for the generated contract entries. Only `src` (and the theme
|
|
11
|
+
// `parent`) are typed today; the rest stays open as the engine is typed
|
|
12
|
+
// progressively. The contract itself is validated at runtime by validateContract.
|
|
13
|
+
interface ModuleDefinition {
|
|
14
|
+
src: string;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ThemeDefinition {
|
|
19
|
+
src: string;
|
|
20
|
+
parent?: string;
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Cached contract, invalidated by the file's mtime. A per-process `const` read
|
|
25
|
+
// at import time never picked up a regenerated contract (e.g. a module enabled
|
|
26
|
+
// while a long-lived dev server runs), so caches keyed only by theme served
|
|
27
|
+
// stale results. Reloading on mtime change makes the contract — and the hash
|
|
28
|
+
// derived from it — track the file.
|
|
29
|
+
let cached = null; // { mtimeMs, config, hash }
|
|
30
|
+
|
|
31
|
+
function fail(message, extra = []) {
|
|
32
|
+
console.error(message);
|
|
33
|
+
extra.forEach((line) => console.error(line));
|
|
34
|
+
console.error(REGENERATE_HINT);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadContract() {
|
|
39
|
+
let stats;
|
|
40
|
+
try {
|
|
41
|
+
stats = fs.statSync(DEPENDENCY_CONFIG_FILE_PATH);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
fail(
|
|
44
|
+
`Error reading configuration file at ${DEPENDENCY_CONFIG_FILE_PATH}: ${error.message}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
49
|
+
return cached.config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let config;
|
|
53
|
+
try {
|
|
54
|
+
config = JSON.parse(fs.readFileSync(DEPENDENCY_CONFIG_FILE_PATH, "utf-8"));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
fail(
|
|
57
|
+
`Error reading or parsing configuration file at ${DEPENDENCY_CONFIG_FILE_PATH}: ${error.message}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const validation = validateContract(config);
|
|
62
|
+
if (!validation.ok) {
|
|
63
|
+
fail(`Invalid MageObsidian frontend contract at ${DEPENDENCY_CONFIG_FILE_PATH}:`, [
|
|
64
|
+
...validation.errors.map((message) => ` - ${message}`),
|
|
65
|
+
"If the version differs, the PHP module and JS engine are out of sync — align their versions, then regenerate.",
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Hash the parts that change the build graph (which modules/themes exist and
|
|
70
|
+
// their resolution). Caches downstream key on this so a changed module set
|
|
71
|
+
// invalidates them, rather than serving a result keyed only by theme name.
|
|
72
|
+
const hash = crypto
|
|
73
|
+
.createHash("sha1")
|
|
74
|
+
.update(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
modules: config.modules,
|
|
77
|
+
themes: config.themes,
|
|
78
|
+
allModules: config.allModules,
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
.digest("hex");
|
|
82
|
+
|
|
83
|
+
cached = { mtimeMs: stats.mtimeMs, config, hash };
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const getContract = () => loadContract();
|
|
88
|
+
|
|
89
|
+
export const getMagentoConfig = () => getContract();
|
|
90
|
+
export const getContractHash = () => {
|
|
91
|
+
getContract();
|
|
92
|
+
return cached.hash;
|
|
93
|
+
};
|
|
94
|
+
export const getModulesConfigArray = () =>
|
|
95
|
+
Object.entries(getContract().modules) as [string, ModuleDefinition][];
|
|
96
|
+
export const getThemesConfigArray = () =>
|
|
97
|
+
Object.entries(getContract().themes) as [string, ThemeDefinition][];
|
|
98
|
+
export const getAllMagentoModulesEnabled = () => getContract().allModules;
|
|
99
|
+
export const isDev = () => getContract().mode !== "production";
|
|
100
|
+
|
|
101
|
+
export const getOutputDirFromTheme = (themePath) => path.resolve(themePath, OUTPUT_THEME_DIR);
|
|
102
|
+
|
|
103
|
+
export const getModuleDefinition = (moduleName): ModuleDefinition =>
|
|
104
|
+
getContract().modules[moduleName];
|
|
105
|
+
|
|
106
|
+
export const getThemeDefinition = (themeName): ThemeDefinition => getContract().themes[themeName];
|
|
107
|
+
|
|
108
|
+
export const MODE = process.env.NODE_ENV;
|
|
109
|
+
|
|
110
|
+
export function resolveLibPath(lib) {
|
|
111
|
+
return path.join(getContract().LIB_PATH, lib);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolveNodePath(packageName) {
|
|
115
|
+
try {
|
|
116
|
+
const resolvedPath = import.meta.resolve(packageName);
|
|
117
|
+
return resolvedPath.replace("file://", "");
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`The package ${packageName} can't be resolved.`);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resolveLibRealPath(lib) {
|
|
125
|
+
return MODE === "production" ? lib : resolveNodePath(lib);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
getMagentoConfig,
|
|
130
|
+
getContractHash,
|
|
131
|
+
getModulesConfigArray,
|
|
132
|
+
getThemesConfigArray,
|
|
133
|
+
getAllMagentoModulesEnabled,
|
|
134
|
+
isDev,
|
|
135
|
+
getOutputDirFromTheme,
|
|
136
|
+
getModuleDefinition,
|
|
137
|
+
getThemeDefinition,
|
|
138
|
+
resolveLibPath,
|
|
139
|
+
resolveNodePath,
|
|
140
|
+
resolveLibRealPath,
|
|
141
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Contract version this build engine understands. Must match
|
|
2
|
+
// ConfigInterface::SCHEMA_VERSION on the PHP side; a mismatch means the module
|
|
3
|
+
// and the JS engine are out of sync and the contract cannot be trusted.
|
|
4
|
+
export const EXPECTED_SCHEMA_VERSION = "1.0.0";
|
|
5
|
+
|
|
6
|
+
// Top-level keys the engine reads off the generated contract. Kept in step with
|
|
7
|
+
// the JSON schema in module-modern-frontend/src/etc.
|
|
8
|
+
export const REQUIRED_CONTRACT_KEYS = [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"mode",
|
|
11
|
+
"modules",
|
|
12
|
+
"themes",
|
|
13
|
+
"allModules",
|
|
14
|
+
"VUE_COMPONENTS_PATH",
|
|
15
|
+
"JS_PATH",
|
|
16
|
+
"FOLDERS_TO_WATCH",
|
|
17
|
+
"ALLOWED_EXTENSIONS",
|
|
18
|
+
"MODULE_CSS_EXTEND_FILE",
|
|
19
|
+
"MODULE_CONFIG_FILE",
|
|
20
|
+
"THEME_CONFIG_FILE",
|
|
21
|
+
"THEME_CSS_SOURCE_FILE",
|
|
22
|
+
"THEME_FILES_PATH",
|
|
23
|
+
"LIB_PATH",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate the parsed frontend contract.
|
|
28
|
+
*
|
|
29
|
+
* Pure: no IO, no process exit. The caller decides how to react to errors.
|
|
30
|
+
*
|
|
31
|
+
* @param {unknown} config Parsed contract object.
|
|
32
|
+
* @param {string} [expectedVersion] Schema version the caller expects.
|
|
33
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
34
|
+
*/
|
|
35
|
+
export function validateContract(config, expectedVersion = EXPECTED_SCHEMA_VERSION) {
|
|
36
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) {
|
|
37
|
+
return { ok: false, errors: ["Contract is not a JSON object."] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const errors = [];
|
|
41
|
+
|
|
42
|
+
if (config.schema_version !== expectedVersion) {
|
|
43
|
+
errors.push(
|
|
44
|
+
`Schema version mismatch: this build engine expects "${expectedVersion}" ` +
|
|
45
|
+
`but the contract is "${config.schema_version ?? "(missing)"}".`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const key of REQUIRED_CONTRACT_KEYS) {
|
|
50
|
+
if (!Object.prototype.hasOwnProperty.call(config, key)) {
|
|
51
|
+
errors.push(`Missing required key "${key}".`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { ok: errors.length === 0, errors };
|
|
56
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import themeResolver from "./themeResolverSync.ts";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
MODULE_TEMPLATES_PATH,
|
|
5
|
+
MODULE_WEB_PATH,
|
|
6
|
+
THEME_CSS_FOLDER,
|
|
7
|
+
THEME_MODULE_WEB_PATH,
|
|
8
|
+
} from "../config/default.ts";
|
|
9
|
+
import { resolveFileByTheme, getAllJsVueFilesWithInheritanceCached } from "./moduleResolver.ts";
|
|
10
|
+
import configResolver from "./configResolver.ts";
|
|
11
|
+
import fs from "node:fs/promises";
|
|
12
|
+
|
|
13
|
+
const { getMagentoConfig, getModuleDefinition, getThemeDefinition, getModulesConfigArray } =
|
|
14
|
+
configResolver;
|
|
15
|
+
|
|
16
|
+
const MODULE_CSS_EXTEND_FILE = getMagentoConfig().MODULE_CSS_EXTEND_FILE;
|
|
17
|
+
const THEME_CSS_SOURCE_FILE = getMagentoConfig().THEME_CSS_SOURCE_FILE;
|
|
18
|
+
|
|
19
|
+
async function getThemeImports(themeName, themeConfig?) {
|
|
20
|
+
if (!themeConfig) {
|
|
21
|
+
themeConfig = await themeResolver.getThemeConfig(themeName);
|
|
22
|
+
}
|
|
23
|
+
const themeDefinition = getThemeDefinition(themeName);
|
|
24
|
+
const themePath = themeDefinition.src;
|
|
25
|
+
|
|
26
|
+
let imports = "";
|
|
27
|
+
if (themeConfig.includeCssSourceFromParentThemes && themeDefinition.parent) {
|
|
28
|
+
imports += await getThemeImports(themeDefinition.parent, themeConfig);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
imports += `@import "${path.join(themePath, THEME_MODULE_WEB_PATH, THEME_CSS_FOLDER, THEME_CSS_SOURCE_FILE)}";\n`;
|
|
32
|
+
return imports;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveModuleCssSourcePath(moduleName, themeName) {
|
|
36
|
+
let moduleConfigSourcePath = resolveFileByTheme(
|
|
37
|
+
themeName,
|
|
38
|
+
moduleName,
|
|
39
|
+
path.join("css", MODULE_CSS_EXTEND_FILE),
|
|
40
|
+
);
|
|
41
|
+
if (!moduleConfigSourcePath) {
|
|
42
|
+
const moduleDefinition = getModuleDefinition(moduleName);
|
|
43
|
+
moduleConfigSourcePath = path.join(
|
|
44
|
+
moduleDefinition.src,
|
|
45
|
+
MODULE_WEB_PATH,
|
|
46
|
+
"css",
|
|
47
|
+
MODULE_CSS_EXTEND_FILE,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return moduleConfigSourcePath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Walk the theme -> parent chain emitting a @source for each theme root so
|
|
54
|
+
// Tailwind scans the Twig/phtml templates of every theme in the inheritance
|
|
55
|
+
// chain (a child theme's classes plus everything it inherits unchanged).
|
|
56
|
+
async function getThemeTemplateSources(themeName) {
|
|
57
|
+
const theme = getThemeDefinition(themeName);
|
|
58
|
+
if (!theme) return "";
|
|
59
|
+
|
|
60
|
+
let sources = "";
|
|
61
|
+
if (theme.parent) {
|
|
62
|
+
sources += await getThemeTemplateSources(theme.parent);
|
|
63
|
+
}
|
|
64
|
+
sources += `@source "${theme.src}";\n`;
|
|
65
|
+
return sources;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Reads the source-scan opt-out: a theme (merged with its parents) lists the
|
|
69
|
+
// modules whose files Tailwind must NOT scan via `ignoredTailwindConfigFromModules`
|
|
70
|
+
// in theme.config.js — an array of module names, or "all" to skip every module.
|
|
71
|
+
// The theme's own files are always scanned.
|
|
72
|
+
async function getScanExclusions(themeName) {
|
|
73
|
+
const themeConfig = await themeResolver.getThemeConfig(themeName);
|
|
74
|
+
const ignored = themeConfig?.ignoredTailwindConfigFromModules;
|
|
75
|
+
return {
|
|
76
|
+
ignoreAllModules: ignored === "all",
|
|
77
|
+
excludedModules: new Set(Array.isArray(ignored) ? ignored : []),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Templates (.twig/.phtml) are not auto-scanned by Tailwind in this pipeline
|
|
82
|
+
// (its base path is the Vite root, not the Magento tree), and the engine only
|
|
83
|
+
// emits @source for JS/Vue components. This registers the template dirs of
|
|
84
|
+
// every compatible module plus the whole theme inheritance chain, mirroring how
|
|
85
|
+
// components already resolve module + parent-theme sources, and honours the
|
|
86
|
+
// per-module scan opt-out.
|
|
87
|
+
async function getTemplateSources(themeName) {
|
|
88
|
+
const { ignoreAllModules, excludedModules } = await getScanExclusions(themeName);
|
|
89
|
+
let sources = "";
|
|
90
|
+
|
|
91
|
+
if (!ignoreAllModules) {
|
|
92
|
+
for (const [moduleName, moduleConfig] of getModulesConfigArray()) {
|
|
93
|
+
if (excludedModules.has(moduleName)) continue;
|
|
94
|
+
const templatesDir = path.join(moduleConfig.src, MODULE_TEMPLATES_PATH);
|
|
95
|
+
try {
|
|
96
|
+
await fs.access(templatesDir);
|
|
97
|
+
sources += `@source "${templatesDir}";\n`;
|
|
98
|
+
} catch {
|
|
99
|
+
// module ships no frontend templates
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
sources += await getThemeTemplateSources(themeName);
|
|
105
|
+
return sources;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function getVueComponentsSource(themeName) {
|
|
109
|
+
const { ignoreAllModules, excludedModules } = await getScanExclusions(themeName);
|
|
110
|
+
const vueComponents = await getAllJsVueFilesWithInheritanceCached(themeName);
|
|
111
|
+
// Inheritance keys look like "Vendor_Module/components/X" or "Theme/...";
|
|
112
|
+
// "Theme" entries are the theme's own files and are always scanned.
|
|
113
|
+
const vueComponentSources = Object.entries(vueComponents)
|
|
114
|
+
.filter(([key]) => {
|
|
115
|
+
const owner = key.split("/")[0];
|
|
116
|
+
if (owner === "Theme") return true;
|
|
117
|
+
if (ignoreAllModules) return false;
|
|
118
|
+
return !excludedModules.has(owner);
|
|
119
|
+
})
|
|
120
|
+
.map(([, url]) => `@source "${url}";\n`);
|
|
121
|
+
return vueComponentSources.join("");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function getCssImports(themeName) {
|
|
125
|
+
const themeConfig = await themeResolver.getThemeConfig(themeName);
|
|
126
|
+
const excludedModules = new Set(themeConfig.ignoredCssFromModules || []);
|
|
127
|
+
const modulesConfig = getModulesConfigArray();
|
|
128
|
+
|
|
129
|
+
const cssSource = await getVueComponentsSource(themeName);
|
|
130
|
+
let cssImports = "";
|
|
131
|
+
|
|
132
|
+
if (themeConfig.ignoredCssFromModules !== "all") {
|
|
133
|
+
for (const [moduleName] of modulesConfig) {
|
|
134
|
+
if (excludedModules.has(moduleName)) continue;
|
|
135
|
+
|
|
136
|
+
const filePath = resolveModuleCssSourcePath(moduleName, themeName);
|
|
137
|
+
try {
|
|
138
|
+
await fs.access(filePath);
|
|
139
|
+
cssImports += `@import "${filePath}";\n`;
|
|
140
|
+
} catch {
|
|
141
|
+
// ignore if file doesn't exist
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
cssImports += await getThemeImports(themeName);
|
|
147
|
+
|
|
148
|
+
const templateSources = await getTemplateSources(themeName);
|
|
149
|
+
return `${cssSource}${templateSources}\n${cssImports}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default getCssImports;
|
|
153
|
+
export { getTemplateSources };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { pathToFileURL
|
|
3
|
-
import themeResolver from
|
|
4
|
-
import moduleResolver from
|
|
5
|
-
import interceptorManager from
|
|
6
|
-
import configResolver from "
|
|
2
|
+
import { pathToFileURL } from "url";
|
|
3
|
+
import themeResolver from "./themeResolverSync.ts";
|
|
4
|
+
import moduleResolver from "./moduleResolver.ts";
|
|
5
|
+
import interceptorManager from "../runtime/interceptorManager.ts";
|
|
6
|
+
import configResolver from "./configResolver.ts";
|
|
7
7
|
|
|
8
8
|
const interceptorsRegisteredByTheme = new Map();
|
|
9
9
|
const generatedInterceptorsCache = new Map();
|
|
10
|
-
export const KEY_INTERCEPTED =
|
|
10
|
+
export const KEY_INTERCEPTED = "originalIntercepted";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Resolves the absolute path of a file using the cached file map
|
|
@@ -16,7 +16,7 @@ export const KEY_INTERCEPTED = 'originalIntercepted';
|
|
|
16
16
|
* @returns {string|null}
|
|
17
17
|
*/
|
|
18
18
|
function resolvePathFromMap(identifier, fileMap) {
|
|
19
|
-
const [moduleName, relativePath] = identifier.split(
|
|
19
|
+
const [moduleName, relativePath] = identifier.split("::");
|
|
20
20
|
if (!moduleName || !relativePath) return null;
|
|
21
21
|
|
|
22
22
|
const parsed = path.parse(relativePath);
|
|
@@ -26,7 +26,7 @@ function resolvePathFromMap(identifier, fileMap) {
|
|
|
26
26
|
return fileMap[key] || null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async function registerInterceptors(themeName) {
|
|
29
|
+
async function registerInterceptors(themeName): Promise<Record<string, any[]>> {
|
|
30
30
|
if (interceptorsRegisteredByTheme.has(themeName)) {
|
|
31
31
|
return interceptorsRegisteredByTheme.get(themeName);
|
|
32
32
|
}
|
|
@@ -38,8 +38,10 @@ async function registerInterceptors(themeName) {
|
|
|
38
38
|
return {};
|
|
39
39
|
}
|
|
40
40
|
// Map<Target, Map<PluginName, PluginConfig>>
|
|
41
|
-
const interceptorsMap = new Map();
|
|
42
|
-
for (const [interceptorName, interceptorDefinition] of Object.entries(
|
|
41
|
+
const interceptorsMap = new Map<string, Map<string, any>>();
|
|
42
|
+
for (const [interceptorName, interceptorDefinition] of Object.entries(
|
|
43
|
+
modulesConfig.interceptors,
|
|
44
|
+
) as [string, any][]) {
|
|
43
45
|
const { target } = interceptorDefinition;
|
|
44
46
|
if (!interceptorName || !target) continue;
|
|
45
47
|
|
|
@@ -59,10 +61,10 @@ async function registerInterceptors(themeName) {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
// Convert Map to structured object and sort plugins
|
|
62
|
-
const result = {};
|
|
64
|
+
const result: Record<string, any[]> = {};
|
|
63
65
|
for (const [target, pluginsMap] of interceptorsMap) {
|
|
64
66
|
const plugins = Array.from(pluginsMap.values())
|
|
65
|
-
.filter(p => p.active !== false) // Filter out inactive plugins
|
|
67
|
+
.filter((p) => p.active !== false) // Filter out inactive plugins
|
|
66
68
|
.sort((a, b) => (a.sortOrder || 10) - (b.sortOrder || 10));
|
|
67
69
|
|
|
68
70
|
if (plugins.length > 0) {
|
|
@@ -82,11 +84,13 @@ async function generateInterceptors(themeName) {
|
|
|
82
84
|
const interceptorsConfig = await registerInterceptors(themeName);
|
|
83
85
|
|
|
84
86
|
// Get all files map from cache
|
|
85
|
-
let allFilesMap
|
|
87
|
+
let allFilesMap;
|
|
86
88
|
try {
|
|
87
89
|
allFilesMap = moduleResolver.getAllJsVueFilesWithInheritanceCached(themeName);
|
|
88
90
|
} catch (e) {
|
|
89
|
-
console.warn(
|
|
91
|
+
console.warn(
|
|
92
|
+
`Cache not found for theme ${themeName} (${e.message}), falling back to async generation.`,
|
|
93
|
+
);
|
|
90
94
|
allFilesMap = await moduleResolver.getAllJsVueFilesWithInheritance(themeName);
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -124,7 +128,10 @@ async function generateInterceptors(themeName) {
|
|
|
124
128
|
try {
|
|
125
129
|
interceptorModule = await import(pathToFileURL(interceptorPath).href);
|
|
126
130
|
} catch (e) {
|
|
127
|
-
console.error(
|
|
131
|
+
console.error(
|
|
132
|
+
`Failed to import interceptor module ${interceptorConfig.interceptor}:`,
|
|
133
|
+
e.message,
|
|
134
|
+
);
|
|
128
135
|
continue;
|
|
129
136
|
}
|
|
130
137
|
|
|
@@ -132,33 +139,37 @@ async function generateInterceptors(themeName) {
|
|
|
132
139
|
for (const interceptorExport of Object.keys(interceptorModule)) {
|
|
133
140
|
let type, targetMethod;
|
|
134
141
|
|
|
135
|
-
if (interceptorExport.startsWith(
|
|
136
|
-
type =
|
|
142
|
+
if (interceptorExport.startsWith("before")) {
|
|
143
|
+
type = "before";
|
|
137
144
|
targetMethod = interceptorExport.substring(6);
|
|
138
|
-
} else if (interceptorExport.startsWith(
|
|
139
|
-
type =
|
|
145
|
+
} else if (interceptorExport.startsWith("around")) {
|
|
146
|
+
type = "around";
|
|
140
147
|
targetMethod = interceptorExport.substring(6);
|
|
141
|
-
} else if (interceptorExport.startsWith(
|
|
142
|
-
type =
|
|
148
|
+
} else if (interceptorExport.startsWith("after")) {
|
|
149
|
+
type = "after";
|
|
143
150
|
targetMethod = interceptorExport.substring(5);
|
|
144
151
|
} else {
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
// Check if target method exists in target module
|
|
149
|
-
if (!targetExports.includes(targetMethod) && targetMethod !==
|
|
156
|
+
if (!targetExports.includes(targetMethod) && targetMethod !== "default") {
|
|
150
157
|
const lowerFirst = targetMethod.charAt(0).toLowerCase() + targetMethod.slice(1);
|
|
151
158
|
if (targetExports.includes(lowerFirst)) {
|
|
152
159
|
targetMethod = lowerFirst;
|
|
153
160
|
} else {
|
|
154
161
|
// Skip invalid methods but don't crash the whole process?
|
|
155
162
|
// User requested error if not found.
|
|
156
|
-
throw new Error(
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Interceptor ${interceptorConfig.name} (${interceptorConfig.interceptor}) exports '${interceptorExport}' but target ${target} does not export '${targetMethod}'`,
|
|
165
|
+
);
|
|
157
166
|
}
|
|
158
167
|
}
|
|
159
168
|
|
|
160
|
-
if (typeof targetModule[targetMethod] !==
|
|
161
|
-
console.warn(
|
|
169
|
+
if (typeof targetModule[targetMethod] !== "function") {
|
|
170
|
+
console.warn(
|
|
171
|
+
`Interceptor ${interceptorConfig.name} (${interceptorConfig.interceptor}) exports '${interceptorExport}' but target ${target} export '${targetMethod}' is not a function.`,
|
|
172
|
+
);
|
|
162
173
|
continue;
|
|
163
174
|
}
|
|
164
175
|
|
|
@@ -168,7 +179,7 @@ async function generateInterceptors(themeName) {
|
|
|
168
179
|
interceptorConfig.name,
|
|
169
180
|
type,
|
|
170
181
|
interceptorModule[interceptorExport],
|
|
171
|
-
interceptorConfig.sortOrder
|
|
182
|
+
interceptorConfig.sortOrder,
|
|
172
183
|
);
|
|
173
184
|
|
|
174
185
|
methodsToIntercept.add(targetMethod);
|
|
@@ -176,7 +187,7 @@ async function generateInterceptors(themeName) {
|
|
|
176
187
|
exportName: interceptorExport,
|
|
177
188
|
type,
|
|
178
189
|
targetMethod,
|
|
179
|
-
sortOrder: interceptorConfig.sortOrder
|
|
190
|
+
sortOrder: interceptorConfig.sortOrder,
|
|
180
191
|
});
|
|
181
192
|
}
|
|
182
193
|
|
|
@@ -184,7 +195,7 @@ async function generateInterceptors(themeName) {
|
|
|
184
195
|
validInterceptors.push({
|
|
185
196
|
...interceptorConfig,
|
|
186
197
|
path: interceptorPath,
|
|
187
|
-
methods: interceptorMethods
|
|
198
|
+
methods: interceptorMethods,
|
|
188
199
|
});
|
|
189
200
|
}
|
|
190
201
|
}
|
|
@@ -194,14 +205,19 @@ async function generateInterceptors(themeName) {
|
|
|
194
205
|
const wrapper = { ...targetModule };
|
|
195
206
|
const proxy = interceptorManager.intercept(wrapper, target, true);
|
|
196
207
|
|
|
197
|
-
const source = generateInterceptorCode(
|
|
208
|
+
const source = generateInterceptorCode(
|
|
209
|
+
target,
|
|
210
|
+
targetPath,
|
|
211
|
+
validInterceptors,
|
|
212
|
+
targetExports,
|
|
213
|
+
);
|
|
198
214
|
|
|
199
215
|
interceptors[target] = {
|
|
200
216
|
proxy,
|
|
201
217
|
targetPath,
|
|
202
218
|
targetModule,
|
|
203
219
|
interceptors: validInterceptors,
|
|
204
|
-
source
|
|
220
|
+
source,
|
|
205
221
|
};
|
|
206
222
|
}
|
|
207
223
|
}
|
|
@@ -219,41 +235,58 @@ function generateInterceptorCode(target, targetPath, interceptors, targetExports
|
|
|
219
235
|
// Using a relative path from this service file might not work in the generated code context.
|
|
220
236
|
// We'll assume '@mage-obsidian/plugin-manager' or similar alias is set up,
|
|
221
237
|
// or use the absolute path which Vite handles.
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
// All string literals go through JSON.stringify so paths or interceptor
|
|
239
|
+
// names containing quotes/backslashes can't break (or inject into) the
|
|
240
|
+
// generated module. Identifiers (export names) come from Object.keys of a
|
|
241
|
+
// real ES module, so they are valid JS and used unquoted.
|
|
242
|
+
const resolvedInterceptorManagerPath = configResolver.resolveLibRealPath(
|
|
243
|
+
"mage-obsidian/runtime/interceptorManager",
|
|
244
|
+
);
|
|
245
|
+
imports.push(
|
|
246
|
+
`import interceptorManager from ${JSON.stringify(resolvedInterceptorManagerPath)};`,
|
|
247
|
+
);
|
|
224
248
|
|
|
225
249
|
// Import Original Module
|
|
226
|
-
imports.push(
|
|
250
|
+
imports.push(
|
|
251
|
+
`import * as originalModule from ${JSON.stringify(`/@fs${targetPath}?${KEY_INTERCEPTED}`)};`,
|
|
252
|
+
);
|
|
227
253
|
|
|
228
254
|
// Import Interceptors
|
|
229
255
|
interceptors.forEach((interceptor, index) => {
|
|
230
256
|
const interceptorVar = `interceptor_${index}`;
|
|
231
|
-
imports.push(
|
|
257
|
+
imports.push(
|
|
258
|
+
`import * as ${interceptorVar} from ${JSON.stringify(`/@fs${interceptor.path}`)};`,
|
|
259
|
+
);
|
|
232
260
|
|
|
233
|
-
interceptor.methods.forEach(method => {
|
|
261
|
+
interceptor.methods.forEach((method) => {
|
|
234
262
|
const methodKey = `${target}::${method.targetMethod}`;
|
|
235
|
-
|
|
263
|
+
const sortOrder = Number.isFinite(method.sortOrder) ? method.sortOrder : 10;
|
|
264
|
+
registrations.push(
|
|
265
|
+
`interceptorManager.addInterceptor(${JSON.stringify(methodKey)}, ${JSON.stringify(interceptor.name)}, ${JSON.stringify(method.type)}, ${interceptorVar}.${method.exportName}, ${sortOrder});`,
|
|
266
|
+
);
|
|
236
267
|
});
|
|
237
268
|
});
|
|
238
269
|
|
|
239
270
|
// Create Interceptor
|
|
240
271
|
const interceptorCode = `
|
|
241
272
|
const targetWrapper = { ...originalModule };
|
|
242
|
-
const proxy = interceptorManager.intercept(targetWrapper,
|
|
273
|
+
const proxy = interceptorManager.intercept(targetWrapper, ${JSON.stringify(target)}, true);
|
|
243
274
|
`;
|
|
244
275
|
|
|
245
276
|
// Exports
|
|
246
|
-
const exportsCode = targetExports
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
277
|
+
const exportsCode = targetExports
|
|
278
|
+
.map((exp) => {
|
|
279
|
+
if (exp === "default") {
|
|
280
|
+
return `export default proxy.default;`;
|
|
281
|
+
}
|
|
282
|
+
return `export const ${exp} = proxy.${exp};`;
|
|
283
|
+
})
|
|
284
|
+
.join("\n");
|
|
252
285
|
|
|
253
286
|
return `
|
|
254
|
-
${imports.join(
|
|
287
|
+
${imports.join("\n")}
|
|
255
288
|
|
|
256
|
-
${registrations.join(
|
|
289
|
+
${registrations.join("\n")}
|
|
257
290
|
|
|
258
291
|
${interceptorCode}
|
|
259
292
|
|
|
@@ -264,5 +297,5 @@ ${exportsCode}
|
|
|
264
297
|
export default {
|
|
265
298
|
registerInterceptors,
|
|
266
299
|
generateInterceptors,
|
|
267
|
-
KEY_INTERCEPTED
|
|
300
|
+
KEY_INTERCEPTED,
|
|
268
301
|
};
|