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,168 @@
1
+ import path from "node:path";
2
+ import moduleResolver from "./moduleResolver.ts";
3
+
4
+ // Map of `from=>to[,from2=>to2]` prefixes, read from the environment. It rewrites
5
+ // the absolute paths baked into the generated jsconfig so go-to-definition and
6
+ // npm resolution land on files where the editor actually runs. This matters when
7
+ // the build runs in a container but the editor opens the same files at a
8
+ // different mount (e.g. theme sources served at /var/www/html in the container
9
+ // but edited at the host bind-mount). An empty map is the identity — correct
10
+ // when the build and editor share a filesystem.
11
+ function parsePathMap(raw?: string): Array<[string, string]> {
12
+ if (!raw) return [];
13
+ return (
14
+ raw
15
+ .split(",")
16
+ .map((pair) => pair.split("=>").map((part) => part.trim()))
17
+ .filter(
18
+ (parts): parts is [string, string] =>
19
+ parts.length === 2 && !!parts[0] && !!parts[1],
20
+ )
21
+ .map(([from, to]) => [from, to] as [string, string])
22
+ // Longest `from` first so a more specific mount (e.g. the vite harness
23
+ // overlaid at /var/www/html/vite) wins over a broader one
24
+ // (/var/www/html) regardless of declaration order.
25
+ .sort((a, b) => b[0].length - a[0].length)
26
+ );
27
+ }
28
+
29
+ function remapPath(filePath: string, map: Array<[string, string]>): string {
30
+ for (const [from, to] of map) {
31
+ if (filePath.startsWith(from)) return to + filePath.slice(from.length);
32
+ }
33
+ return filePath;
34
+ }
35
+
36
+ // The Vite harness install (the `vite/` dir is the cwd during a build) is where
37
+ // the build resolves bare npm specifiers, so point the editor at the same place.
38
+ // Wildcard so `vue`, `@heroicons/vue/...`, `pinia` and friends all resolve.
39
+ function nodeModulesWildcard(map: Array<[string, string]>): string {
40
+ return path.join(remapPath(path.resolve(process.cwd(), "node_modules"), map), "*");
41
+ }
42
+
43
+ // Engine subpath imports (`mage-obsidian/runtime/*`, `/service/*`, …) route
44
+ // through the package's `exports` (`./runtime/*` → `./src/runtime/*`), which the
45
+ // bare `"*"` wildcard's literal substitution would miss — it would look under the
46
+ // package root, not `src/`. A more-specific `mage-obsidian/*` → `src/*` pattern
47
+ // resolves them (TS prefers the longer match over `"*"`).
48
+ function engineSrcWildcard(map: Array<[string, string]>): string {
49
+ return path.join(
50
+ remapPath(path.resolve(process.cwd(), "node_modules", "mage-obsidian", "src"), map),
51
+ "*",
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Build the `compilerOptions.paths` map that makes the framework's
57
+ * `Vendor_Module::` import specifiers first-class for the editor: each maps
58
+ * directly to its real source file, so Ctrl+click jumps straight there and a
59
+ * typo surfaces as an unresolved import. Derived from the same inheritance cache
60
+ * the resolver uses, so the declared set matches exactly what the build resolves
61
+ * (`.ts`/`.js`/`.vue` — assets are not in the cache). A `"*"` wildcard adds npm
62
+ * package resolution from the harness node_modules.
63
+ */
64
+ export default async function generateJsconfigPaths(
65
+ themeName: string,
66
+ options: { remap?: boolean } = {},
67
+ ): Promise<Record<string, string[]>> {
68
+ const files = (await moduleResolver.getAllJsVueFilesWithInheritance(themeName)) as Record<
69
+ string,
70
+ string
71
+ >;
72
+ // The editor jsconfig remaps container paths to the host mount so go-to-def
73
+ // lands on the files the editor opens. The typecheck gate runs where the build
74
+ // runs (same filesystem as the sources), so it opts out and keeps real paths.
75
+ const map =
76
+ options.remap === false ? [] : parsePathMap(process.env.MAGE_OBSIDIAN_TYPES_PATH_MAP);
77
+
78
+ const paths: Record<string, string[]> = {
79
+ "mage-obsidian/*": [engineSrcWildcard(map)],
80
+ "*": [nodeModulesWildcard(map)],
81
+ };
82
+
83
+ for (const [key, absolutePath] of Object.entries(files)) {
84
+ const slash = key.indexOf("/");
85
+ if (slash === -1) continue;
86
+ const namespace = key.slice(0, slash);
87
+ const rest = key.slice(slash + 1);
88
+ const target = remapPath(absolutePath, map);
89
+
90
+ paths[`${namespace}::${rest}`] = [target];
91
+ if (rest.startsWith("components/")) {
92
+ // The resolver accepts the shorthand without the `components/` prefix.
93
+ paths[`${namespace}::${rest.slice("components/".length)}`] = [target];
94
+ }
95
+ }
96
+
97
+ return paths;
98
+ }
99
+
100
+ const GENERATED_MARKER = "// Generated by mage-obsidian";
101
+
102
+ /**
103
+ * Wrap a `paths` map in the full theme jsconfig the editor consumes. `allowJs`
104
+ * is required or the theme's `.js`/`.vue` sources never join the TS program (so
105
+ * nothing resolves); `baseUrl` is deliberately omitted — it is deprecated and a
106
+ * hard config error in TS 7.0, which makes the editor discard the whole config.
107
+ * The leading marker comment lets the build tell its own output apart from a
108
+ * hand-written jsconfig and refuse to clobber the latter.
109
+ */
110
+ function buildThemeJsconfig(paths: Record<string, string[]>): string {
111
+ const jsconfig = {
112
+ compilerOptions: {
113
+ allowJs: true,
114
+ checkJs: false,
115
+ module: "esnext",
116
+ moduleResolution: "bundler",
117
+ paths,
118
+ },
119
+ include: ["**/*.ts", "**/*.js", "**/*.vue"],
120
+ exclude: ["**/generated/**", "node_modules"],
121
+ };
122
+ const header = `${GENERATED_MARKER} — do not edit. Refreshed on every build and live in dev.\n`;
123
+ return `${header}${JSON.stringify(jsconfig, null, 4)}\n`;
124
+ }
125
+
126
+ /**
127
+ * Wrap a `paths` map in the theme's typecheck tsconfig — the CI/build gate run by
128
+ * `vue-tsc --noEmit`. Distinct from the editor `jsconfig.json` (same dir) on
129
+ * purpose: it is NOT named `tsconfig.json`, so the editor's auto-discovery keeps
130
+ * using the jsconfig while this file is consumed only via an explicit `-p`. It is
131
+ * the one context where every `Vendor_Module::` resolves to its real source (the
132
+ * `paths` are the same map the build resolves through), so cross-module type
133
+ * seams are actually verified.
134
+ *
135
+ * `allowImportingTsExtensions` is required because engine imports use explicit
136
+ * `.ts` specifiers; it in turn requires `noEmit`. `allowJs`/`checkJs:false` keep
137
+ * not-yet-migrated `.js` in the graph (resolvable) without type-checking them, so
138
+ * a half-migrated tree still passes. `lib` carries DOM because the storefront
139
+ * code touches `document`/`window`/`fetch`/`localStorage`.
140
+ *
141
+ * `files` is the exact assembled set the build bundles (the inheritance-resolved
142
+ * absolute paths), not a recursive theme-relative glob: module sources live
143
+ * outside the theme dir, so such a glob would silently check nothing. Listing
144
+ * them explicitly is what makes the gate type-check every module/override file.
145
+ */
146
+ function buildThemeTsconfig(paths: Record<string, string[]>, files: string[]): string {
147
+ const tsconfig = {
148
+ compilerOptions: {
149
+ noEmit: true,
150
+ allowImportingTsExtensions: true,
151
+ module: "esnext",
152
+ moduleResolution: "bundler",
153
+ target: "esnext",
154
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
155
+ allowJs: true,
156
+ checkJs: false,
157
+ skipLibCheck: true,
158
+ strict: false,
159
+ paths,
160
+ },
161
+ files,
162
+ exclude: ["**/generated/**", "node_modules"],
163
+ };
164
+ const header = `${GENERATED_MARKER} — typecheck gate (vue-tsc). Build-managed; do not edit.\n`;
165
+ return `${header}${JSON.stringify(tsconfig, null, 4)}\n`;
166
+ }
167
+
168
+ export { buildThemeJsconfig, buildThemeTsconfig, GENERATED_MARKER };
@@ -1,41 +1,49 @@
1
1
  import path from "path";
2
- import configResolver from "./configResolver.js";
2
+ import configResolver from "./configResolver.ts";
3
3
  import {
4
4
  ALL_JS_VUE_FILES_WITH_INHERITANCE_FILE_NAME,
5
5
  MODULE_WEB_PATH,
6
6
  PRECOMPILED_FOLDER,
7
- THEME_MODULE_WEB_PATH
8
- } from "../config/default.js";
9
- import getFilesFromFolders from '../utils/findComponents.js'
7
+ THEME_MODULE_WEB_PATH,
8
+ } from "../config/default.ts";
9
+ import getFilesFromFolders from "../utils/findComponents.ts";
10
10
  import fs from "fs";
11
11
  import deepmerge from "deepmerge";
12
12
 
13
13
  const defaultFoldersToMap = [
14
14
  {
15
15
  src: configResolver.getMagentoConfig().VUE_COMPONENTS_PATH,
16
- ext: [
17
- 'vue',
18
- 'js'
19
- ]
16
+ ext: ["vue", "ts", "js"],
20
17
  },
21
18
  {
22
19
  src: configResolver.getMagentoConfig().JS_PATH,
23
- ext: [
24
- 'js'
25
- ]
26
- }
20
+ ext: ["ts", "js"],
21
+ },
27
22
  ];
28
23
 
29
- const cachedModulesByTheme = {};
24
+ // Keyed by `${theme}\0${contractHash}` so a changed module/theme set (new hash)
25
+ // recomputes instead of returning a result keyed only by theme name.
26
+ const cachedModulesByTheme = new Map();
27
+
28
+ // NUL separator: theme names and the hex hash never contain it, so the parts
29
+ // can't collide. Shared with invalidateTheme so its prefix match stays in
30
+ // lock-step with the key format (a plain space here silently broke the match).
31
+ const CACHE_KEY_SEP = "\0";
32
+ const cacheKey = (themeName) => `${themeName}${CACHE_KEY_SEP}${configResolver.getContractHash()}`;
33
+
34
+ // Caches the persisted inheritance map keyed by theme, invalidated by the file's
35
+ // mtime so a rebuilt .precompiled JSON (watch mode, regeneration) is picked up
36
+ // instead of serving a stale in-memory copy.
37
+ const precompiledFileCache = new Map();
30
38
 
31
39
  async function getAllJsVueFilesFromActiveModules() {
32
- let result = {};
40
+ const result = {};
33
41
  for (const entry of configResolver.getModulesConfigArray()) {
34
42
  const [moduleName, moduleConfig] = entry;
35
43
  const moduleResult = await getFilesFromFolders(
36
44
  moduleName,
37
45
  path.resolve(moduleConfig.src, MODULE_WEB_PATH),
38
- defaultFoldersToMap
46
+ defaultFoldersToMap,
39
47
  );
40
48
  Object.assign(result, moduleResult);
41
49
  }
@@ -46,59 +54,77 @@ async function getAllJsVueFilesFromTheme(themeName) {
46
54
  let result = {};
47
55
  const theme = configResolver.getMagentoConfig().themes[themeName];
48
56
  if (!theme) {
49
- return {}
57
+ return {};
50
58
  }
51
59
 
52
60
  if (theme.parent) {
53
- result = await getAllJsVueFilesFromTheme(theme.parent)
61
+ result = await getAllJsVueFilesFromTheme(theme.parent);
54
62
  }
55
63
 
56
64
  for (const moduleName of configResolver.getAllMagentoModulesEnabled()) {
57
65
  const modulePath = path.resolve(theme.src, moduleName, THEME_MODULE_WEB_PATH);
58
- const moduleResult = await getFilesFromFolders(
59
- moduleName,
60
- modulePath,
61
- defaultFoldersToMap
62
- );
66
+ const moduleResult = await getFilesFromFolders(moduleName, modulePath, defaultFoldersToMap);
63
67
  if (moduleResult) {
64
68
  result = deepmerge(result, moduleResult);
65
69
  }
66
70
  }
67
71
  const moduleResult = await getFilesFromFolders(
68
- 'Theme',
72
+ configResolver.getMagentoConfig().THEME_FILES_PATH,
69
73
  path.resolve(theme.src, THEME_MODULE_WEB_PATH),
70
- defaultFoldersToMap
74
+ defaultFoldersToMap,
71
75
  );
72
76
  result = deepmerge(result, moduleResult);
73
77
  return result;
74
78
  }
75
79
 
76
-
77
- function getAllJsVueFilesWithInheritanceCached(themeName) {
80
+ function getAllJsVueFilesWithInheritanceCached(themeName?) {
78
81
  themeName = themeName ?? process.env.CURRENT_THEME;
79
- if (cachedModulesByTheme[themeName]) {
80
- return cachedModulesByTheme[themeName];
82
+ const filePath = path.join(
83
+ PRECOMPILED_FOLDER,
84
+ themeName,
85
+ ALL_JS_VUE_FILES_WITH_INHERITANCE_FILE_NAME,
86
+ );
87
+ const { mtimeMs } = fs.statSync(filePath);
88
+ const cached = precompiledFileCache.get(themeName);
89
+ if (cached && cached.mtimeMs === mtimeMs) {
90
+ return cached.data;
81
91
  }
82
- const filePath = path.join(PRECOMPILED_FOLDER, themeName, ALL_JS_VUE_FILES_WITH_INHERITANCE_FILE_NAME);
83
- const fileContent = fs.readFileSync(filePath, "utf-8");
84
- const data = JSON.parse(fileContent);
85
- cachedModulesByTheme[themeName] = data;
92
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
93
+ precompiledFileCache.set(themeName, { mtimeMs, data });
86
94
  return data;
87
95
  }
88
96
 
89
- async function getAllJsVueFilesWithInheritance(themeName) {
97
+ async function getAllJsVueFilesWithInheritance(themeName?) {
90
98
  themeName = themeName ?? process.env.CURRENT_THEME;
91
- if (cachedModulesByTheme[themeName]) {
92
- return cachedModulesByTheme[themeName];
99
+ const key = cacheKey(themeName);
100
+ if (cachedModulesByTheme.has(key)) {
101
+ return cachedModulesByTheme.get(key);
93
102
  }
94
103
  const allJsVueFilesFromModules = await getAllJsVueFilesFromActiveModules();
95
104
  const allJsVueFilesFromThemes = await getAllJsVueFilesFromTheme(themeName);
96
- cachedModulesByTheme[themeName] = deepmerge(allJsVueFilesFromModules, allJsVueFilesFromThemes);
97
- return cachedModulesByTheme[themeName];
105
+ const merged = deepmerge(allJsVueFilesFromModules, allJsVueFilesFromThemes);
106
+ cachedModulesByTheme.set(key, merged);
107
+ return merged;
98
108
  }
99
109
 
100
110
  const moduleConfigByThemeCache = new Map();
101
111
 
112
+ // Drop every cached entry for a theme so the next resolution recomputes from
113
+ // disk. Adding or removing a component/js source changes neither the contract
114
+ // nor its hash, so the (theme, hash)-keyed caches would otherwise serve a stale
115
+ // map; the dev-server watcher calls this before regenerating on a source change.
116
+ function invalidateTheme(themeName?) {
117
+ themeName = themeName ?? process.env.CURRENT_THEME;
118
+ const prefix = `${themeName}${CACHE_KEY_SEP}`;
119
+ for (const key of cachedModulesByTheme.keys()) {
120
+ if (key.startsWith(prefix)) cachedModulesByTheme.delete(key);
121
+ }
122
+ for (const key of moduleConfigByThemeCache.keys()) {
123
+ if (key.startsWith(prefix)) moduleConfigByThemeCache.delete(key);
124
+ }
125
+ precompiledFileCache.delete(themeName);
126
+ }
127
+
102
128
  const MODULE_CONFIG_FILE = configResolver.getMagentoConfig().MODULE_CONFIG_FILE;
103
129
 
104
130
  /**
@@ -107,12 +133,12 @@ const MODULE_CONFIG_FILE = configResolver.getMagentoConfig().MODULE_CONFIG_FILE;
107
133
  * @returns {string} Full path to the configuration file.
108
134
  */
109
135
  function getModuleConfigPath(moduleSrc) {
110
- return path.join(moduleSrc, 'view/frontend/web/', MODULE_CONFIG_FILE);
136
+ return path.join(moduleSrc, "view/frontend/web/", MODULE_CONFIG_FILE);
111
137
  }
112
138
 
113
139
  function resolveFileByTheme(themeName, moduleName, filePath) {
114
140
  const theme = configResolver.getMagentoConfig().themes[themeName];
115
- const fullFilePath = path.join(theme.src, moduleName, 'web', filePath);
141
+ const fullFilePath = path.join(theme.src, moduleName, "web", filePath);
116
142
  if (fs.existsSync(fullFilePath)) {
117
143
  return fullFilePath;
118
144
  }
@@ -150,20 +176,21 @@ async function resolveModuleConfig(moduleName, themeName) {
150
176
  * @returns {object|null} Tailwind configuration object.
151
177
  */
152
178
  async function getModuleConfigByThemeConfig(themeName, themeConfig) {
153
- if (moduleConfigByThemeCache.has(themeName)) {
154
- return moduleConfigByThemeCache.get(themeName);
179
+ const key = cacheKey(themeName);
180
+ if (moduleConfigByThemeCache.has(key)) {
181
+ return moduleConfigByThemeCache.get(key);
155
182
  }
156
183
  if (!themeConfig) return null;
157
184
  const modules = configResolver.getAllMagentoModulesEnabled();
158
185
  let modulesConfig = {};
159
186
 
160
187
  for (const moduleName of modules) {
161
- let moduleConfig = await resolveModuleConfig(moduleName, themeName);
188
+ const moduleConfig = await resolveModuleConfig(moduleName, themeName);
162
189
  if (!moduleConfig) continue;
163
190
  modulesConfig = deepmerge(modulesConfig, moduleConfig);
164
191
  }
165
192
 
166
- moduleConfigByThemeCache.set(themeName, modulesConfig);
193
+ moduleConfigByThemeCache.set(key, modulesConfig);
167
194
  return modulesConfig;
168
195
  }
169
196
 
@@ -171,8 +198,14 @@ export default {
171
198
  getAllJsVueFilesWithInheritance,
172
199
  getAllJsVueFilesWithInheritanceCached,
173
200
  getModuleConfigByThemeConfig,
174
- resolveFileByTheme
201
+ resolveFileByTheme,
202
+ invalidateTheme,
175
203
  };
176
204
 
177
- export { getModuleConfigByThemeConfig, resolveFileByTheme, getAllJsVueFilesWithInheritance, getAllJsVueFilesWithInheritanceCached };
178
-
205
+ export {
206
+ getModuleConfigByThemeConfig,
207
+ resolveFileByTheme,
208
+ getAllJsVueFilesWithInheritance,
209
+ getAllJsVueFilesWithInheritanceCached,
210
+ invalidateTheme,
211
+ };
@@ -0,0 +1,105 @@
1
+ import path from "path";
2
+ import fs from "fs/promises";
3
+ import moduleResolver from "./moduleResolver.ts";
4
+ import {
5
+ PRECOMPILED_FOLDER,
6
+ ALL_JS_VUE_FILES_WITH_INHERITANCE_FILE_NAME,
7
+ } from "../config/default.ts";
8
+ import getCssImports from "./cssResolver.ts";
9
+ import configResolver from "./configResolver.ts";
10
+ import generateJsconfigPaths, {
11
+ buildThemeJsconfig,
12
+ buildThemeTsconfig,
13
+ GENERATED_MARKER,
14
+ } from "./generateJsconfig.ts";
15
+ import { fileURLToPath } from "url";
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ async function writeToFile(outputPath, content) {
20
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
21
+ await fs.writeFile(outputPath, content, "utf-8");
22
+ }
23
+
24
+ async function precompileCss(themeName) {
25
+ const cssContent = await getCssImports(themeName);
26
+ const outputPath = path.resolve(PRECOMPILED_FOLDER, themeName, "precompiled.css");
27
+ await writeToFile(outputPath, cssContent);
28
+ }
29
+
30
+ async function precompileJs(themeName) {
31
+ const baseJsPath = path.resolve(__dirname, "../templates/base_main_js_file.js");
32
+ const jsOutputPath = path.resolve(PRECOMPILED_FOLDER, themeName, "precompiled.js");
33
+ await fs.copyFile(baseJsPath, jsOutputPath);
34
+
35
+ const vueFilesOutputPath = path.join(
36
+ PRECOMPILED_FOLDER,
37
+ themeName,
38
+ ALL_JS_VUE_FILES_WITH_INHERITANCE_FILE_NAME,
39
+ );
40
+ const vueFiles = await moduleResolver.getAllJsVueFilesWithInheritance(themeName);
41
+ await writeToFile(vueFilesOutputPath, JSON.stringify(vueFiles));
42
+ }
43
+
44
+ // Write a build-managed config into the theme src, refusing to clobber a
45
+ // hand-written one: only (re)writes a file carrying our generated marker, or
46
+ // creates one when absent. Write-if-changed so an unchanged file keeps its mtime
47
+ // (no needless TS-server reload).
48
+ async function writeManagedConfig(outputPath, content, manages) {
49
+ let existing;
50
+ try {
51
+ existing = await fs.readFile(outputPath, "utf-8");
52
+ } catch {
53
+ existing = null;
54
+ }
55
+ if (existing !== null && !existing.startsWith(GENERATED_MARKER)) {
56
+ console.warn(
57
+ `[mage-obsidian] ${outputPath} is not generated by mage-obsidian; leaving it untouched. ` +
58
+ `Remove it to let the build manage ${manages}.`,
59
+ );
60
+ return;
61
+ }
62
+ if (existing === content) return;
63
+ await writeToFile(outputPath, content);
64
+ }
65
+
66
+ // The theme's jsconfig.json (gitignored) so the author's editor — which opens the
67
+ // theme sources directly — resolves the framework's `Vendor_Module::` specifiers
68
+ // and npm packages via `paths`. Skipped silently if the theme has no src.
69
+ async function precompileJsconfig(themeName) {
70
+ const themeDefinition = configResolver.getThemeDefinition(themeName);
71
+ if (!themeDefinition?.src) return;
72
+ const paths = await generateJsconfigPaths(themeName);
73
+ await writeManagedConfig(
74
+ path.join(themeDefinition.src, "jsconfig.json"),
75
+ buildThemeJsconfig(paths),
76
+ "Vendor_Module:: import resolution",
77
+ );
78
+ }
79
+
80
+ // The theme's tsconfig.typecheck.json (gitignored) — the assembled `vue-tsc`
81
+ // gate. Same `paths` the build resolves through, so every `Vendor_Module::`
82
+ // resolves and cross-module type seams are actually checked. Not named
83
+ // `tsconfig.json`, so it never shadows the editor jsconfig.
84
+ async function precompileTsconfig(themeName) {
85
+ const themeDefinition = configResolver.getThemeDefinition(themeName);
86
+ if (!themeDefinition?.src) return;
87
+ const paths = await generateJsconfigPaths(themeName, { remap: false });
88
+ // The assembled file set to check: every resolved target from the literal
89
+ // `Vendor_Module::` keys, deduped (shorthands point at the same file). Wildcard
90
+ // keys (`*`, `mage-obsidian/*`) are resolution patterns, not source files.
91
+ const files = [
92
+ ...new Set(
93
+ Object.entries(paths)
94
+ .filter(([key]) => !key.includes("*"))
95
+ .flatMap(([, targets]) => targets),
96
+ ),
97
+ ];
98
+ await writeManagedConfig(
99
+ path.join(themeDefinition.src, "tsconfig.typecheck.json"),
100
+ buildThemeTsconfig(paths, files),
101
+ "the vue-tsc typecheck gate",
102
+ );
103
+ }
104
+
105
+ export { precompileCss, precompileJs, precompileJsconfig, precompileTsconfig };
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import {
4
+ precompileCss,
5
+ precompileJs,
6
+ precompileJsconfig,
7
+ precompileTsconfig,
8
+ } from "./preCompileFiles.ts";
9
+ import configResolver from "./configResolver.ts";
10
+ import { PRECOMPILED_FOLDER } from "../config/default.ts";
11
+
12
+ // Guard per (theme, contract hash): repeated calls within one process (Vite
13
+ // re-invokes the config) skip the work, while a different theme — or the same
14
+ // theme after the contract changed (e.g. a module enabled/disabled mid dev
15
+ // server) — re-precompiles. Keying on the hash, not just the theme name, makes
16
+ // the invalidation dimension explicit instead of relying on process isolation.
17
+ const precompiledByTheme = new Map();
18
+
19
+ export default async (themeName) => {
20
+ const contractHash = configResolver.getContractHash();
21
+ if (precompiledByTheme.get(themeName) === contractHash) {
22
+ return;
23
+ }
24
+ const themeDir = path.resolve(PRECOMPILED_FOLDER, themeName);
25
+ // Clean only this theme's output. The old `rm -rf .precompiled/*` wiped
26
+ // every theme, which would corrupt a concurrent or sibling-theme build.
27
+ await fs.rm(themeDir, { recursive: true, force: true });
28
+ await fs.mkdir(themeDir, { recursive: true });
29
+ await precompileJs(themeName);
30
+ await precompileCss(themeName);
31
+ await precompileJsconfig(themeName);
32
+ await precompileTsconfig(themeName);
33
+ precompiledByTheme.set(themeName, contractHash);
34
+ };
@@ -0,0 +1,66 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import deepmerge from "deepmerge";
4
+ import { THEME_MODULE_WEB_PATH } from "../config/default.ts";
5
+ import configResolver from "./configResolver.ts";
6
+
7
+ const themeConfigCache = new Map();
8
+ const themeConfigPlainCache = new Map();
9
+
10
+ function getThemeConfigPath(themeSrc) {
11
+ return path.join(
12
+ themeSrc,
13
+ THEME_MODULE_WEB_PATH,
14
+ configResolver.getMagentoConfig().THEME_CONFIG_FILE,
15
+ );
16
+ }
17
+
18
+ async function loadThemeConfig(themeDefinition, themeName) {
19
+ if (!themeDefinition) return null;
20
+ if (themeConfigPlainCache.has(themeName)) return themeConfigPlainCache.get(themeName);
21
+
22
+ try {
23
+ const configPath = getThemeConfigPath(themeDefinition.src);
24
+ fs.accessSync(configPath, fs.constants.F_OK);
25
+
26
+ let themeConfig = await import(pathToFileUrl(configPath));
27
+ themeConfig = themeConfig.default ?? themeConfig;
28
+
29
+ themeConfigPlainCache.set(themeName, themeConfig);
30
+ return deepmerge({}, themeConfig);
31
+ } catch (error) {
32
+ console.error(`Failed to load configuration for theme "${themeName}":`, error.message);
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function pathToFileUrl(filePath) {
38
+ const resolvedPath = path.resolve(filePath);
39
+ const fileUrl = new URL(`file://${resolvedPath}`);
40
+ return fileUrl.href;
41
+ }
42
+
43
+ export async function getThemeConfig(themeName) {
44
+ if (themeConfigCache.has(themeName)) return themeConfigCache.get(themeName);
45
+
46
+ const themeDefinition = configResolver.getMagentoConfig().themes[themeName];
47
+ if (!themeDefinition) return null;
48
+
49
+ let themeConfig = await loadThemeConfig(themeDefinition, themeName);
50
+ if (!themeConfig) return null;
51
+
52
+ themeConfig.includeCssSourceFromParentThemes ??= true;
53
+ themeConfig.ignoredCssFromModules ??= [];
54
+ themeConfig.exposeNpmPackages ??= [];
55
+ if (themeDefinition.parent) {
56
+ const parentConfig = await getThemeConfig(themeDefinition.parent);
57
+ themeConfig = deepmerge(parentConfig || {}, themeConfig);
58
+ }
59
+ themeConfigCache.set(themeName, themeConfig);
60
+ return themeConfig;
61
+ }
62
+
63
+ // Export default con todo
64
+ export default {
65
+ getThemeConfig,
66
+ };