@adobe-commerce/elsie 1.9.0-beta.1 → 1.9.0-beta.3
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/CHANGELOG.md +12 -0
- package/config/vite.mjs +258 -81
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @adobe-commerce/elsie
|
|
2
2
|
|
|
3
|
+
## 1.9.0-beta.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5c64620: Implement a new `fragment-import-redirect` build plugin that automatically detects and redirects any dropin source file that directly imports a fragment source file (bypassing the barrel). The import is silently redirected to the fragments barrel at build time and a warning is emitted identifying the file so it can be corrected in source. This ensures fragment constants always appear as local declarations in `fragments.js` regardless of how dropin source code references them.
|
|
8
|
+
|
|
9
|
+
## 1.9.0-beta.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- d2aacc7: Fix: GraphQL fragment source files are no longer incorrectly bundled into `chunks/api.js`. The `manualChunks` function now walks the full importer graph (with cycle protection) to determine whether an api-directory module is owned by the fragments barrel, so fragment files stay in the fragments output chunk even when accessed through intermediate sub-barrels. Boilerplate GraphQL overrides work correctly in all dropin barrel structures.
|
|
14
|
+
|
|
3
15
|
## 1.9.0-beta.1
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/config/vite.mjs
CHANGED
|
@@ -55,7 +55,7 @@ const paths = {
|
|
|
55
55
|
)
|
|
56
56
|
: undefined,
|
|
57
57
|
|
|
58
|
-
components: elsieConfig.components?.map((component) =>
|
|
58
|
+
components: elsieConfig.components?.map((component) =>
|
|
59
59
|
path.resolve(process.cwd(), component.root)
|
|
60
60
|
),
|
|
61
61
|
};
|
|
@@ -93,6 +93,245 @@ if (paths.fragments) {
|
|
|
93
93
|
input.fragments = path.resolve(paths.fragments);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Recursively walks the Rollup importer graph upward to determine whether
|
|
98
|
+
* `id` is reachable from `targetPath` at any depth.
|
|
99
|
+
*
|
|
100
|
+
* Used by `manualChunks` to exclude fragment source files from the api chunk:
|
|
101
|
+
* any module that is (transitively) imported by the fragments barrel must stay
|
|
102
|
+
* in the fragments output so the gql-extend override mechanism can replace it
|
|
103
|
+
* independently of api.js.
|
|
104
|
+
*
|
|
105
|
+
* A `visited` Set is threaded through each recursive call to short-circuit
|
|
106
|
+
* cycles in the module graph. It is created fresh per `manualChunks` invocation
|
|
107
|
+
* so no state leaks between module evaluations.
|
|
108
|
+
*/
|
|
109
|
+
function isReachableFrom(targetPath, id, getModuleInfo, visited = new Set()) {
|
|
110
|
+
if (visited.has(id)) return false;
|
|
111
|
+
visited.add(id);
|
|
112
|
+
const info = getModuleInfo(id);
|
|
113
|
+
return info?.importers?.some(
|
|
114
|
+
(imp) => imp === targetPath || isReachableFrom(targetPath, imp, getModuleInfo, visited)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Vite plugin that enforces the fragments barrel import contract at build time.
|
|
120
|
+
*
|
|
121
|
+
* Fragment source files (those re-exported by the fragments barrel) must be
|
|
122
|
+
* imported exclusively through the barrel. This guarantees Rollup emits their
|
|
123
|
+
* GraphQL string constants as local `const` declarations inside `fragments.js`.
|
|
124
|
+
* The gql-extend tool reads and rewrites those declarations at boilerplate build
|
|
125
|
+
* time; if the constants end up in `api.js` instead, the override mechanism
|
|
126
|
+
* silently produces incorrect output.
|
|
127
|
+
*
|
|
128
|
+
* When a dropin source file imports a fragment file directly (bypassing the
|
|
129
|
+
* barrel), this plugin:
|
|
130
|
+
* 1. Transparently redirects the import to the barrel at resolve time, so the
|
|
131
|
+
* build output is always correct regardless of dropin source conventions.
|
|
132
|
+
* 2. Emits a build warning per offending file listing every direct importer,
|
|
133
|
+
* so project maintainers know exactly which imports to correct in source.
|
|
134
|
+
*
|
|
135
|
+
* IMPORTANT — placement: this plugin must be listed BEFORE tsconfigPaths() in
|
|
136
|
+
* the plugins array. Both use `enforce: 'pre'`; within that group Vite respects
|
|
137
|
+
* array order. Being first ensures our resolveId hook intercepts the alias-form
|
|
138
|
+
* import before tsconfigPaths claims it — once tsconfigPaths resolves the alias
|
|
139
|
+
* and returns, our hook is never reached.
|
|
140
|
+
*/
|
|
141
|
+
function fragmentImportRedirectPlugin(paths, elsieConfig) {
|
|
142
|
+
// Per-build state. Both are cleared in buildStart so watch-mode rebuilds
|
|
143
|
+
// always start from a clean slate.
|
|
144
|
+
const fragmentSourceFiles = new Set();
|
|
145
|
+
const fragmentViolations = new Map(); // fragment file → [direct importers]
|
|
146
|
+
|
|
147
|
+
// Pre-compute alias info once so buildStart and buildEnd share the same
|
|
148
|
+
// values without re-reading elsieConfig and without unsafe property access
|
|
149
|
+
// inside hook callbacks (where elsieConfig.api could be undefined).
|
|
150
|
+
const apiAliasRoot = elsieConfig.api?.importAliasRoot;
|
|
151
|
+
const apiRoot = path.resolve(process.cwd(), elsieConfig.api?.root ?? 'src/api');
|
|
152
|
+
// Alias specifier for the barrel itself, e.g. '@/cart/api/fragments'
|
|
153
|
+
const barrelAlias = apiAliasRoot
|
|
154
|
+
? `${apiAliasRoot}/${path.basename(paths.fragments, path.extname(paths.fragments))}`
|
|
155
|
+
: path.relative(process.cwd(), paths.fragments);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: 'fragment-import-redirect',
|
|
159
|
+
enforce: 'pre',
|
|
160
|
+
|
|
161
|
+
buildStart() {
|
|
162
|
+
fragmentSourceFiles.clear();
|
|
163
|
+
fragmentViolations.clear();
|
|
164
|
+
|
|
165
|
+
// Read the barrel and resolve each re-exported specifier to an absolute
|
|
166
|
+
// path. The resulting set is what resolveId checks against.
|
|
167
|
+
const barrelContent = fs.readFileSync(paths.fragments, 'utf-8');
|
|
168
|
+
const barrelDir = path.dirname(paths.fragments);
|
|
169
|
+
const importRegex = /from\s+['"]([^'"]+)['"]/g;
|
|
170
|
+
let match;
|
|
171
|
+
|
|
172
|
+
while ((match = importRegex.exec(barrelContent)) !== null) {
|
|
173
|
+
let specifier = match[1];
|
|
174
|
+
|
|
175
|
+
// Resolve alias-prefixed specifiers (e.g. '@/cart/api/graphql/CartFragment')
|
|
176
|
+
// using the dropin's importAliasRoot config before probing the filesystem.
|
|
177
|
+
if (apiAliasRoot && specifier.startsWith(`${apiAliasRoot}/`)) {
|
|
178
|
+
specifier = path.join(apiRoot, specifier.slice(apiAliasRoot.length + 1));
|
|
179
|
+
} else if (specifier.startsWith('.')) {
|
|
180
|
+
specifier = path.resolve(barrelDir, specifier);
|
|
181
|
+
} else {
|
|
182
|
+
continue; // skip bare specifiers (node_modules re-exports)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Barrel specifiers rarely include file extensions; probe common ones.
|
|
186
|
+
for (const ext of ['', '.ts', '.tsx', '.js']) {
|
|
187
|
+
const candidate = specifier + ext;
|
|
188
|
+
if (fs.existsSync(candidate)) {
|
|
189
|
+
fragmentSourceFiles.add(candidate);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
async resolveId(source, importer) {
|
|
197
|
+
if (!importer || fragmentSourceFiles.size === 0) return null;
|
|
198
|
+
// Allow the barrel to import fragment files (its own re-exports) and
|
|
199
|
+
// allow fragment files to import each other (shared helper fragments).
|
|
200
|
+
if (importer === paths.fragments || fragmentSourceFiles.has(importer)) return null;
|
|
201
|
+
|
|
202
|
+
// Delegate to subsequent plugins (including tsconfigPaths) so alias
|
|
203
|
+
// specifiers are fully resolved to an absolute path before we check.
|
|
204
|
+
// skipSelf prevents infinite recursion back into this hook.
|
|
205
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
206
|
+
if (!resolved || !fragmentSourceFiles.has(resolved.id)) return null;
|
|
207
|
+
|
|
208
|
+
// Track the violation for the buildEnd warning.
|
|
209
|
+
if (!fragmentViolations.has(resolved.id)) fragmentViolations.set(resolved.id, []);
|
|
210
|
+
fragmentViolations.get(resolved.id).push(importer);
|
|
211
|
+
|
|
212
|
+
// Redirect to the barrel. Rollup treats the barrel as the resolved module,
|
|
213
|
+
// so the fragment constants become local declarations in fragments.js.
|
|
214
|
+
return { id: paths.fragments };
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
buildEnd() {
|
|
218
|
+
for (const [fragmentFile, importers] of fragmentViolations) {
|
|
219
|
+
// Derive the wrong specifier from the actual fragment file path so the
|
|
220
|
+
// suggestion shows the real import, not a generic placeholder.
|
|
221
|
+
const relFragment = path.relative(apiRoot, fragmentFile).replace(/\.(ts|tsx|js)$/, '');
|
|
222
|
+
const wrongSpecifier = apiAliasRoot ? `${apiAliasRoot}/${relFragment}` : relFragment;
|
|
223
|
+
|
|
224
|
+
this.warn(
|
|
225
|
+
`\n"${path.relative(process.cwd(), fragmentFile)}" belongs to the fragments barrel ` +
|
|
226
|
+
`but was directly imported by:\n${
|
|
227
|
+
importers.map((imp) => ` - ${path.relative(process.cwd(), imp)}`).join('\n')
|
|
228
|
+
}\n\nThe import was automatically redirected through the fragments barrel. ` +
|
|
229
|
+
`\nUpdate the import in source to silence this warning: '${wrongSpecifier}' → '${barrelAlias}'\n`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Vite plugin that rewrites relative `../src/` paths in generated sourcemaps
|
|
238
|
+
* to package-qualified paths (e.g. `../../src/foo.ts` → `/@scope/pkg/src/foo.ts`).
|
|
239
|
+
*
|
|
240
|
+
* Predictable, package-namespaced paths let browser devtools and error-tracking
|
|
241
|
+
* services (Sentry, Datadog, etc.) resolve source files back to the correct
|
|
242
|
+
* package across CDN-delivered builds where multiple dropin sourcemaps are
|
|
243
|
+
* loaded in the same session.
|
|
244
|
+
*/
|
|
245
|
+
function rewriteSourcemapSourcesPlugin(packageJSON) {
|
|
246
|
+
return {
|
|
247
|
+
name: 'rewrite-sourcemap-sources',
|
|
248
|
+
generateBundle(options, bundle) {
|
|
249
|
+
for (const fileName in bundle) {
|
|
250
|
+
const chunk = bundle[fileName];
|
|
251
|
+
|
|
252
|
+
// Process both standalone .map asset files and inline chunk maps.
|
|
253
|
+
if (
|
|
254
|
+
(chunk.type === 'asset' && fileName.endsWith('.map')) ||
|
|
255
|
+
(chunk.type === 'chunk' && chunk.map)
|
|
256
|
+
) {
|
|
257
|
+
try {
|
|
258
|
+
const map =
|
|
259
|
+
chunk.type === 'asset' ? JSON.parse(chunk.source) : chunk.map;
|
|
260
|
+
|
|
261
|
+
if (map.sources) {
|
|
262
|
+
map.sources = map.sources.map((input) => {
|
|
263
|
+
// Replace any leading `../src/` or `../../src/` traversal with
|
|
264
|
+
// the package name so the path is stable and globally unique.
|
|
265
|
+
return input.replace(
|
|
266
|
+
/(?:\.\.?\/)+src\//,
|
|
267
|
+
`/${packageJSON.name}/src/`
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (chunk.type === 'asset') {
|
|
272
|
+
chunk.source = JSON.stringify(map);
|
|
273
|
+
} else {
|
|
274
|
+
chunk.map = map;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.error('Error transforming sourcemap:', e);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Vite plugin that emits a minimal `package.json`, `LICENSE.md`, and
|
|
288
|
+
* `CHANGELOG.md` into the `dist/` directory alongside the compiled output.
|
|
289
|
+
*
|
|
290
|
+
* The minimal package.json carries only `name`, `version`, and `license` so
|
|
291
|
+
* CDN consumers and package tooling can identify a build artifact without
|
|
292
|
+
* pulling in the full development-time manifest (devDependencies, scripts, etc.).
|
|
293
|
+
* LICENSE.md is required; the build fails early if it is missing.
|
|
294
|
+
*/
|
|
295
|
+
function generateDistPackageJsonPlugin(packageJSON) {
|
|
296
|
+
return {
|
|
297
|
+
name: 'generate-dist-package-json',
|
|
298
|
+
generateBundle() {
|
|
299
|
+
this.emitFile({
|
|
300
|
+
type: 'asset',
|
|
301
|
+
fileName: 'package.json',
|
|
302
|
+
source: JSON.stringify(
|
|
303
|
+
{
|
|
304
|
+
name: packageJSON.name,
|
|
305
|
+
version: packageJSON.version,
|
|
306
|
+
license: packageJSON.license,
|
|
307
|
+
},
|
|
308
|
+
null,
|
|
309
|
+
2
|
|
310
|
+
),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const licensePath = path.resolve(process.cwd(), 'LICENSE.md');
|
|
314
|
+
if (!fs.existsSync(licensePath)) {
|
|
315
|
+
this.error(`LICENSE.md not found at ${licensePath}`);
|
|
316
|
+
}
|
|
317
|
+
this.emitFile({
|
|
318
|
+
type: 'asset',
|
|
319
|
+
fileName: 'LICENSE.md',
|
|
320
|
+
source: fs.readFileSync(licensePath, 'utf-8'),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const changelogPath = path.resolve(process.cwd(), 'CHANGELOG.md');
|
|
324
|
+
if (fs.existsSync(changelogPath)) {
|
|
325
|
+
this.emitFile({
|
|
326
|
+
type: 'asset',
|
|
327
|
+
fileName: 'CHANGELOG.md',
|
|
328
|
+
source: fs.readFileSync(changelogPath, 'utf-8'),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
96
335
|
export default {
|
|
97
336
|
preview: {
|
|
98
337
|
host: true,
|
|
@@ -128,16 +367,24 @@ export default {
|
|
|
128
367
|
entryFileNames: '[name].js',
|
|
129
368
|
assetFileNames: '[name].[ext]',
|
|
130
369
|
chunkFileNames: 'chunks/[name].js',
|
|
131
|
-
manualChunks: (id) => {
|
|
132
|
-
// Fragments
|
|
133
|
-
if (id.includes(paths.fragments)) return 'no-chunk';
|
|
370
|
+
manualChunks: (id, { getModuleInfo }) => {
|
|
371
|
+
// Fragments barrel entry does not accept chunking
|
|
372
|
+
if (paths.fragments && id.includes(paths.fragments)) return 'no-chunk';
|
|
134
373
|
|
|
135
374
|
// API functions → chunks/api.js
|
|
136
|
-
|
|
375
|
+
// All modules under paths.api go to the api chunk, except those imported
|
|
376
|
+
// by the fragments barrel — those must stay in the fragments output so the
|
|
377
|
+
// boilerplate GraphQL override mechanism can replace them independently.
|
|
378
|
+
if (paths.api && id.includes(paths.api)) {
|
|
379
|
+
if (paths.fragments && isReachableFrom(paths.fragments, id, getModuleInfo)) {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
return 'api';
|
|
383
|
+
}
|
|
137
384
|
|
|
138
385
|
// components → chunks/components.js
|
|
139
386
|
if (paths.components?.some((component) => id.includes(component))) return 'components';
|
|
140
|
-
|
|
387
|
+
|
|
141
388
|
return undefined;
|
|
142
389
|
},
|
|
143
390
|
|
|
@@ -204,6 +451,9 @@ export default {
|
|
|
204
451
|
plugins: [
|
|
205
452
|
banner(`Copyright ${currentYear} Adobe\nAll Rights Reserved.`),
|
|
206
453
|
|
|
454
|
+
// Must come before tsconfigPaths — see fragmentImportRedirectPlugin for why.
|
|
455
|
+
paths.fragments ? fragmentImportRedirectPlugin(paths, elsieConfig) : null,
|
|
456
|
+
|
|
207
457
|
tsconfigPaths(),
|
|
208
458
|
|
|
209
459
|
cssInjectedByJsPlugin({
|
|
@@ -303,82 +553,9 @@ export default {
|
|
|
303
553
|
},
|
|
304
554
|
}),
|
|
305
555
|
|
|
306
|
-
|
|
307
|
-
name: 'rewrite-sourcemap-sources',
|
|
308
|
-
generateBundle(options, bundle) {
|
|
309
|
-
for (const fileName in bundle) {
|
|
310
|
-
const chunk = bundle[fileName];
|
|
311
|
-
|
|
312
|
-
// Process both .map files and JS/TS files with sourcemaps
|
|
313
|
-
if (
|
|
314
|
-
(chunk.type === 'asset' && fileName.endsWith('.map')) ||
|
|
315
|
-
(chunk.type === 'chunk' && chunk.map)
|
|
316
|
-
) {
|
|
317
|
-
try {
|
|
318
|
-
// Get the sourcemap object - either from the asset source or the chunk's map
|
|
319
|
-
const map =
|
|
320
|
-
chunk.type === 'asset' ? JSON.parse(chunk.source) : chunk.map;
|
|
321
|
-
|
|
322
|
-
if (map.sources) {
|
|
323
|
-
map.sources = map.sources.map((input) => {
|
|
324
|
-
return input.replace(
|
|
325
|
-
/(?:\.\.?\/)+src\//,
|
|
326
|
-
`/${packageJSON.name}/src/`
|
|
327
|
-
);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// Update the sourcemap in the appropriate place
|
|
331
|
-
if (chunk.type === 'asset') {
|
|
332
|
-
chunk.source = JSON.stringify(map);
|
|
333
|
-
} else {
|
|
334
|
-
chunk.map = map;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
} catch (e) {
|
|
338
|
-
console.error('Error transforming sourcemap:', e);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
},
|
|
343
|
-
},
|
|
344
|
-
|
|
345
|
-
{
|
|
346
|
-
name: 'generate-dist-package-json',
|
|
347
|
-
generateBundle() {
|
|
348
|
-
this.emitFile({
|
|
349
|
-
type: 'asset',
|
|
350
|
-
fileName: 'package.json',
|
|
351
|
-
source: JSON.stringify(
|
|
352
|
-
{
|
|
353
|
-
name: packageJSON.name,
|
|
354
|
-
version: packageJSON.version,
|
|
355
|
-
license: packageJSON.license,
|
|
356
|
-
},
|
|
357
|
-
null,
|
|
358
|
-
2
|
|
359
|
-
),
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const licensePath = path.resolve(process.cwd(), 'LICENSE.md');
|
|
363
|
-
if (!fs.existsSync(licensePath)) {
|
|
364
|
-
this.error(`LICENSE.md not found at ${licensePath}`);
|
|
365
|
-
}
|
|
366
|
-
this.emitFile({
|
|
367
|
-
type: 'asset',
|
|
368
|
-
fileName: 'LICENSE.md',
|
|
369
|
-
source: fs.readFileSync(licensePath, 'utf-8'),
|
|
370
|
-
});
|
|
556
|
+
rewriteSourcemapSourcesPlugin(packageJSON),
|
|
371
557
|
|
|
372
|
-
|
|
373
|
-
if (fs.existsSync(changelogPath)) {
|
|
374
|
-
this.emitFile({
|
|
375
|
-
type: 'asset',
|
|
376
|
-
fileName: 'CHANGELOG.md',
|
|
377
|
-
source: fs.readFileSync(changelogPath, 'utf-8'),
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
},
|
|
558
|
+
generateDistPackageJsonPlugin(packageJSON),
|
|
382
559
|
|
|
383
560
|
process.env.ANALYZE
|
|
384
561
|
? visualizer({
|