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.
Files changed (111) hide show
  1. package/.github/workflows/ci.yml +38 -0
  2. package/.github/workflows/publish.yml +26 -25
  3. package/.oxlintrc.json +19 -0
  4. package/.prettierignore +7 -0
  5. package/.prettierrc.json +7 -0
  6. package/LICENSE +1 -1
  7. package/README.md +1 -0
  8. package/eslint.config.js +24 -0
  9. package/package.json +39 -13
  10. package/pnpm-workspace.yaml +4 -0
  11. package/src/cli/buildThemes.ts +263 -0
  12. package/src/config/default.ts +31 -0
  13. package/src/config/eslint.js +60 -0
  14. package/src/core/configResolver.ts +141 -0
  15. package/src/core/contractValidator.ts +56 -0
  16. package/src/core/cssResolver.ts +153 -0
  17. package/src/{service/generateInterceptors.js → core/generateInterceptors.ts} +79 -46
  18. package/src/core/generateJsconfig.ts +168 -0
  19. package/src/{service/moduleResolver.js → core/moduleResolver.ts} +79 -46
  20. package/src/core/preCompileFiles.ts +105 -0
  21. package/src/core/preCompileMagentoFiles.ts +34 -0
  22. package/src/core/themeResolverSync.ts +66 -0
  23. package/src/runtime/i18nCore.ts +113 -0
  24. package/src/runtime/interceptorManager.ts +186 -0
  25. package/src/runtime/islands.ts +84 -0
  26. package/src/runtime/sectionStoreCore.ts +259 -0
  27. package/src/utils/{findComponents.js → findComponents.ts} +29 -16
  28. package/src/utils/runWithConcurrency.ts +35 -0
  29. package/src/vite/defaultNodeResolver.ts +17 -0
  30. package/src/{service/inheritAssetsModuleResolver.js → vite/inheritAssetsModuleResolver.ts} +15 -11
  31. package/src/{service/inheritModuleResolver.js → vite/inheritModuleResolver.ts} +6 -10
  32. package/src/{service/interceptorsPlugin.js → vite/interceptorsPlugin.ts} +25 -19
  33. package/src/vite/magentoHrmRewrite.ts +44 -0
  34. package/src/vite/sharedPlugins.ts +54 -0
  35. package/src/vite/themeSourceWatcher.ts +96 -0
  36. package/src/vite/unresolvedModuleGuard.ts +87 -0
  37. package/tsconfig.json +34 -0
  38. package/vitest.config.js +18 -0
  39. package/jest.config.js +0 -5
  40. package/src/__tests__/__mocks__/configResolver.js +0 -35
  41. package/src/__tests__/fixtures/interceptors/emptyTarget.js +0 -2
  42. package/src/__tests__/fixtures/interceptors/pluginForNonFunction.js +0 -7
  43. package/src/__tests__/fixtures/interceptors/pluginModule.js +0 -13
  44. package/src/__tests__/fixtures/interceptors/targetModule.js +0 -12
  45. package/src/__tests__/fixtures/interceptors/targetNonFunction.js +0 -7
  46. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/components/ComponentA.vue +0 -0
  47. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/components/ComponentB.vue +0 -0
  48. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/css/module.extend.css +0 -0
  49. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/js/main.js +0 -0
  50. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/main.js +0 -0
  51. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/module.config.js +0 -9
  52. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/components/ComponentA.vue +0 -0
  53. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/components/ComponentB.vue +0 -0
  54. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/css/module.extend.css +0 -2
  55. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/js/main.js +0 -0
  56. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/web/module.config.js +0 -7
  57. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/components/ComponentA.vue +0 -0
  58. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/components/ComponentB.vue +0 -0
  59. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/css/module.extend.css +0 -2
  60. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/js/main.js +0 -0
  61. package/src/__tests__/magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/web/module.config.js +0 -7
  62. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/components/ComponentA.vue +0 -0
  63. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/components/ComponentB.vue +0 -0
  64. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/css/module.extend.css +0 -0
  65. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/js/main.js +0 -0
  66. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/module.config.js +0 -2
  67. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/components/ComponentA.vue +0 -0
  68. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/components/ComponentB.vue +0 -0
  69. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/css/module.extend.css +0 -0
  70. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/js/main.js +0 -0
  71. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/module.config.js +0 -2
  72. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/components/ComponentA.vue +0 -0
  73. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/components/ComponentB.vue +0 -0
  74. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/css/module.extend.css +0 -0
  75. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/js/main.js +0 -0
  76. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/module.config.js +0 -2
  77. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/components/ComponentA.vue +0 -0
  78. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/components/ComponentB.vue +0 -0
  79. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/css/theme.source.css +0 -6
  80. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/js/main.js +0 -0
  81. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/theme.config.js +0 -5
  82. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-b/web/css/theme.source.css +0 -6
  83. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-b/web/theme.config.js +0 -5
  84. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/components/ComponentA.vue +0 -0
  85. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/components/ComponentB.vue +0 -0
  86. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/css/module.extend.css +0 -0
  87. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/js/main.js +0 -0
  88. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/module.config.js +0 -2
  89. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/web/css/theme.source.css +0 -6
  90. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/web/theme.config.js +0 -2
  91. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-d/web/css/theme.source.css +0 -6
  92. package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-d/web/theme.config.js +0 -3
  93. package/src/__tests__/magento_scenarios/app/etc/mage_obsidian_frontend_modules_a.json +0 -53
  94. package/src/__tests__/magento_scenarios/app/etc/mage_obsidian_frontend_modules_b.json +0 -50
  95. package/src/__tests__/service/generateInterceptors.test.js +0 -196
  96. package/src/__tests__/service/inheritModuleResolver.test.js +0 -61
  97. package/src/__tests__/service/interceptorManager_magento_behavior.test.js +0 -137
  98. package/src/__tests__/service/moduleResolver.test.js +0 -194
  99. package/src/__tests__/service/themeResolverSync.test.js +0 -97
  100. package/src/__tests__/vite-plugins/interceptorsPlugin.test.js +0 -121
  101. package/src/config/default.js +0 -27
  102. package/src/scripts/buildThemes.js +0 -132
  103. package/src/service/configResolver.js +0 -63
  104. package/src/service/cssResolver.js +0 -73
  105. package/src/service/defaultNodeResolver.js +0 -16
  106. package/src/service/interceptorManager.js +0 -137
  107. package/src/service/magentoHrmRewrite.js +0 -43
  108. package/src/service/preCompileFiles.js +0 -37
  109. package/src/service/preCompileMagentoFiles.js +0 -16
  110. package/src/service/setupGlobals.js +0 -19
  111. 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, fileURLToPath } from "url";
3
- import themeResolver from 'mage-obsidian/service/themeResolverSync.js';
4
- import moduleResolver from 'mage-obsidian/service/moduleResolver.js';
5
- import interceptorManager from 'mage-obsidian/service/interceptorManager.js';
6
- import configResolver from "mage-obsidian/service/configResolver.js";
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 = 'originalIntercepted';
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( modulesConfig.interceptors)) {
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(`Cache not found for theme ${themeName}, falling back to async generation.`);
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(`Failed to import interceptor module ${interceptorConfig.interceptor}:`, e.message);
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('before')) {
136
- type = 'before';
142
+ if (interceptorExport.startsWith("before")) {
143
+ type = "before";
137
144
  targetMethod = interceptorExport.substring(6);
138
- } else if (interceptorExport.startsWith('around')) {
139
- type = 'around';
145
+ } else if (interceptorExport.startsWith("around")) {
146
+ type = "around";
140
147
  targetMethod = interceptorExport.substring(6);
141
- } else if (interceptorExport.startsWith('after')) {
142
- type = 'after';
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 !== 'default') {
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(`Interceptor ${interceptorConfig.name} (${interceptorConfig.interceptor}) exports '${interceptorExport}' but target ${target} does not export '${targetMethod}'`);
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] !== 'function') {
161
- console.warn(`Interceptor ${interceptorConfig.name} (${interceptorConfig.interceptor}) exports '${interceptorExport}' but target ${target} export '${targetMethod}' is not a function.`);
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(target, targetPath, validInterceptors, targetExports);
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
- const resolvedInterceptorManagerPath = configResolver.resolveLibRealPath('mage-obsidian/service/interceptorManager');
223
- imports.push(`import interceptorManager from "${resolvedInterceptorManagerPath}";`);
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(`import * as originalModule from '/@fs${targetPath}?${KEY_INTERCEPTED}';`);
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(`import * as ${interceptorVar} from '/@fs${interceptor.path}';`);
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
- registrations.push(`interceptorManager.addInterceptor('${methodKey}', '${interceptor.name}', '${method.type}', ${interceptorVar}.${method.exportName}, ${method.sortOrder});`);
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, '${target}', true);
273
+ const proxy = interceptorManager.intercept(targetWrapper, ${JSON.stringify(target)}, true);
243
274
  `;
244
275
 
245
276
  // Exports
246
- const exportsCode = targetExports.map(exp => {
247
- if (exp === 'default') {
248
- return `export default proxy.default;`;
249
- }
250
- return `export const ${exp} = proxy.${exp};`;
251
- }).join('\n');
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('\n')}
287
+ ${imports.join("\n")}
255
288
 
256
- ${registrations.join('\n')}
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
  };