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,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.
|
|
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.
|
|
9
|
-
import getFilesFromFolders from
|
|
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
|
-
|
|
25
|
-
]
|
|
26
|
-
}
|
|
20
|
+
ext: ["ts", "js"],
|
|
21
|
+
},
|
|
27
22
|
];
|
|
28
23
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
|
83
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
+
};
|