@adobe-commerce/elsie 1.9.0-beta.2 → 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 +6 -0
- package/config/vite.mjs +236 -76
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 1.9.0-beta.2
|
|
4
10
|
|
|
5
11
|
### Patch 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,19 @@ 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
|
+
*/
|
|
96
109
|
function isReachableFrom(targetPath, id, getModuleInfo, visited = new Set()) {
|
|
97
110
|
if (visited.has(id)) return false;
|
|
98
111
|
visited.add(id);
|
|
@@ -102,6 +115,223 @@ function isReachableFrom(targetPath, id, getModuleInfo, visited = new Set()) {
|
|
|
102
115
|
);
|
|
103
116
|
}
|
|
104
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
|
+
|
|
105
335
|
export default {
|
|
106
336
|
preview: {
|
|
107
337
|
host: true,
|
|
@@ -221,6 +451,9 @@ export default {
|
|
|
221
451
|
plugins: [
|
|
222
452
|
banner(`Copyright ${currentYear} Adobe\nAll Rights Reserved.`),
|
|
223
453
|
|
|
454
|
+
// Must come before tsconfigPaths — see fragmentImportRedirectPlugin for why.
|
|
455
|
+
paths.fragments ? fragmentImportRedirectPlugin(paths, elsieConfig) : null,
|
|
456
|
+
|
|
224
457
|
tsconfigPaths(),
|
|
225
458
|
|
|
226
459
|
cssInjectedByJsPlugin({
|
|
@@ -320,82 +553,9 @@ export default {
|
|
|
320
553
|
},
|
|
321
554
|
}),
|
|
322
555
|
|
|
323
|
-
|
|
324
|
-
name: 'rewrite-sourcemap-sources',
|
|
325
|
-
generateBundle(options, bundle) {
|
|
326
|
-
for (const fileName in bundle) {
|
|
327
|
-
const chunk = bundle[fileName];
|
|
328
|
-
|
|
329
|
-
// Process both .map files and JS/TS files with sourcemaps
|
|
330
|
-
if (
|
|
331
|
-
(chunk.type === 'asset' && fileName.endsWith('.map')) ||
|
|
332
|
-
(chunk.type === 'chunk' && chunk.map)
|
|
333
|
-
) {
|
|
334
|
-
try {
|
|
335
|
-
// Get the sourcemap object - either from the asset source or the chunk's map
|
|
336
|
-
const map =
|
|
337
|
-
chunk.type === 'asset' ? JSON.parse(chunk.source) : chunk.map;
|
|
338
|
-
|
|
339
|
-
if (map.sources) {
|
|
340
|
-
map.sources = map.sources.map((input) => {
|
|
341
|
-
return input.replace(
|
|
342
|
-
/(?:\.\.?\/)+src\//,
|
|
343
|
-
`/${packageJSON.name}/src/`
|
|
344
|
-
);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// Update the sourcemap in the appropriate place
|
|
348
|
-
if (chunk.type === 'asset') {
|
|
349
|
-
chunk.source = JSON.stringify(map);
|
|
350
|
-
} else {
|
|
351
|
-
chunk.map = map;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
} catch (e) {
|
|
355
|
-
console.error('Error transforming sourcemap:', e);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
|
|
362
|
-
{
|
|
363
|
-
name: 'generate-dist-package-json',
|
|
364
|
-
generateBundle() {
|
|
365
|
-
this.emitFile({
|
|
366
|
-
type: 'asset',
|
|
367
|
-
fileName: 'package.json',
|
|
368
|
-
source: JSON.stringify(
|
|
369
|
-
{
|
|
370
|
-
name: packageJSON.name,
|
|
371
|
-
version: packageJSON.version,
|
|
372
|
-
license: packageJSON.license,
|
|
373
|
-
},
|
|
374
|
-
null,
|
|
375
|
-
2
|
|
376
|
-
),
|
|
377
|
-
});
|
|
556
|
+
rewriteSourcemapSourcesPlugin(packageJSON),
|
|
378
557
|
|
|
379
|
-
|
|
380
|
-
if (!fs.existsSync(licensePath)) {
|
|
381
|
-
this.error(`LICENSE.md not found at ${licensePath}`);
|
|
382
|
-
}
|
|
383
|
-
this.emitFile({
|
|
384
|
-
type: 'asset',
|
|
385
|
-
fileName: 'LICENSE.md',
|
|
386
|
-
source: fs.readFileSync(licensePath, 'utf-8'),
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
const changelogPath = path.resolve(process.cwd(), 'CHANGELOG.md');
|
|
390
|
-
if (fs.existsSync(changelogPath)) {
|
|
391
|
-
this.emitFile({
|
|
392
|
-
type: 'asset',
|
|
393
|
-
fileName: 'CHANGELOG.md',
|
|
394
|
-
source: fs.readFileSync(changelogPath, 'utf-8'),
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
},
|
|
398
|
-
},
|
|
558
|
+
generateDistPackageJsonPlugin(packageJSON),
|
|
399
559
|
|
|
400
560
|
process.env.ANALYZE
|
|
401
561
|
? visualizer({
|