Skip to content

Commit ffc93b7

Browse files
authored
PRO-8768: relative paths for the good operating systems (#5194)
* Back to relative paths for the good operating systems * Fix vite tests
1 parent 730b143 commit ffc93b7

4 files changed

Lines changed: 176 additions & 69 deletions

File tree

‎packages/apostrophe/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Fixes an issue where the frontend was caching stale choices for `select`, `radio` and `checkboxes`
88
fields that were saved but no longer valid (i.e removed from the schema).
99
* Bumped dependency on `express-cache-on-demand` to guarantee installation of recent fixes for edge cases that could share a response between two locales of a single-site project, report errors without a process restart, and correctly handle `res.send('')` with an empty string.
10+
* Restores relative import paths inside Vite-generated entrypoints on *nix builds so dev server HMR works again, while keeping the Windows-specific absolute path workaround.
1011

1112
## 4.24.0 (2025-11-25)
1213

‎packages/apostrophe/modules/@apostrophecms/asset/lib/build/external-module-api.js‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,18 @@ function invoke() {
630630
invokeCode: ''
631631
};
632632

633+
const useAbsoluteImportPaths = process.platform === 'win32';
634+
const toRelativeImportPath = (componentPath) => {
635+
const normalized = componentPath.replace(/\\/g, '/');
636+
if (normalized.startsWith('./') || normalized.startsWith('../')) {
637+
return normalized;
638+
}
639+
if (normalized.startsWith('/')) {
640+
return `.${normalized}`;
641+
}
642+
return `./${normalized}`;
643+
};
644+
633645
components.forEach((entry, i) => {
634646
const { component, path: realPath } = entry;
635647
if (options.requireDefaultExport) {
@@ -653,9 +665,13 @@ function invoke() {
653665
}
654666
}
655667
}
656-
// You would think we should run pathToFileURL over realPath,
657-
// but that actually breaks both Windows and Linux with Vite. -Tom
658-
const importPath = JSON.stringify(realPath);
668+
// Windows needs absolute paths for native Node.js builds whereas
669+
// *nix builds must stay relative to keep Vite's dev server happy.
670+
// pathToFileURL breaks both environments, so stick to plain strings.
671+
const importTarget = (useAbsoluteImportPaths || !component)
672+
? realPath
673+
: toRelativeImportPath(component);
674+
const importPath = JSON.stringify(importTarget);
659675
const name = self.getComponentNameByPath(
660676
component,
661677
{ enumerate: options.enumerateImports === true ? i : false }

‎packages/apostrophe/test/assets.js‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,26 @@ describe('Assets', function() {
119119
assert(apos.asset);
120120
});
121121

122+
it('emits relative entrypoint imports on non-Windows systems', async function() {
123+
if (process.platform === 'win32') {
124+
return this.skip();
125+
}
126+
127+
const componentPath = './modules/example-module/ui/src/index.js';
128+
const absolutePath = path.join(
129+
process.cwd(),
130+
'packages/apostrophe/test/modules/example-module/ui/src/index.js'
131+
);
132+
const { importCode } = apos.asset.getImportFileOutput([
133+
{
134+
component: componentPath,
135+
path: absolutePath
136+
}
137+
]);
138+
139+
assert(importCode.includes(`from "${componentPath}"`));
140+
});
141+
122142
it('should serve static files', async function() {
123143
const text = await apos.http.get('/static-test.txt');
124144
assert(text.match(/served/));

‎packages/vite/test/vite.test.js‎

Lines changed: 136 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -395,85 +395,155 @@ describe('@apostrophecms/vite', function () {
395395
assert.match(adminBarComponentContent, /<h1>The Apos Admin Bar<\/h1>/);
396396

397397
// II. Core Entrypoints
398-
// 1. src.js
399-
{
400-
const match = srcContent.match(/"[^"]+\/@apostrophecms\/admin-bar\/ui\/src\/index.js";/g);
401-
assert.equal(match?.length, 1, 'The core admin-bar module should be imported once');
402-
}
403-
// 2. apos.js
404-
{
405-
const match = aposContent.match(
406-
/import TheAposAdminBar from "[^"]+\/admin-bar-component\/ui\/apos\/components\/TheAposAdminBar\.vue";/g
398+
if (process.platform === 'win32') {
399+
// 1. src.js
400+
{
401+
const match = srcContent.match(/"[^"]+\/@apostrophecms\/admin-bar\/ui\/src\/index.js";/g);
402+
assert.equal(match?.length, 1, 'The core admin-bar module should be imported once');
403+
}
404+
// 2. apos.js
405+
{
406+
const match = aposContent.match(
407+
/import TheAposAdminBar from "[^"]+\/admin-bar-component\/apos\/components\/TheAposAdminBar\.vue";/g
408+
);
409+
assert.equal(match?.length, 1, 'TheAposAdminBar.vue component override should be imported once');
410+
}
411+
{
412+
const match = aposContent.match(
413+
/window\.apos\.vueComponents\["TheAposAdminBar"\] = TheAposAdminBar;/g
414+
);
415+
assert.equal(match?.length, 1, 'TheAposAdminBar.vue component should be registered once');
416+
}
417+
{
418+
const match = aposContent.match(
419+
/import AposAdminBar_[\w\d]+ from "[^"]+\/@apostrophecms\/admin-bar\/ui\/apos\/apps\/AposAdminBar\.js";/g
420+
);
421+
assert.equal(match?.length, 1, 'AposAdminBar.js App import should be present once');
422+
}
423+
{
424+
const match = aposContent.match(
425+
/AposAdminBar_[\d]+App\(\);/g
426+
);
427+
assert.equal(match?.length, 1, 'AposAdminBar.js App should be called once');
428+
}
429+
assert.match(
430+
aposContent,
431+
/import AposCommandMenuKey from "[^"]+\/@apostrophecms\/command-menu\/ui\/apos\/components\/AposCommandMenuKey\.vue";/
407432
);
408-
assert.equal(match?.length, 1, 'TheAposAdminBar.vue component override should be imported once');
409-
}
410-
{
411-
const match = aposContent.match(
412-
/window\.apos\.vueComponents\["TheAposAdminBar"\] = TheAposAdminBar;/g
433+
assert.match(
434+
aposContent,
435+
/import Link from "[^"]+\/@apostrophecms\/rich-text-widget\/ui\/apos\/tiptap-extensions\/Link\.js";/
413436
);
414-
assert.equal(match?.length, 1, 'TheAposAdminBar.vue component should be registered once');
415-
}
416-
{
417-
const match = aposContent.match(
418-
/import AposAdminBar_[\w\d]+ from "[^"]+\/@apostrophecms\/admin-bar\/ui\/apos\/apps\/AposAdminBar\.js";/g
437+
} else {
438+
{
439+
const match = srcContent.match(/".\/@apostrophecms\/admin-bar\/src\/index.js";/g);
440+
assert.equal(match?.length, 1, 'The core admin-bar module should be imported once');
441+
}
442+
{
443+
const match = aposContent.match(
444+
/import TheAposAdminBar from ".\/admin-bar-component\/apos\/components\/TheAposAdminBar\.vue";/g
445+
);
446+
assert.equal(match?.length, 1, 'TheAposAdminBar.vue component override should be imported once');
447+
}
448+
{
449+
const match = aposContent.match(
450+
/window\.apos\.vueComponents\["TheAposAdminBar"\] = TheAposAdminBar;/g
451+
);
452+
assert.equal(match?.length, 1, 'TheAposAdminBar.vue component should be registered once');
453+
}
454+
{
455+
const match = aposContent.match(
456+
/import AposAdminBar_[\w\d]+ from ".\/@apostrophecms\/admin-bar\/apos\/apps\/AposAdminBar\.js";/g
457+
);
458+
assert.equal(match?.length, 1, 'AposAdminBar.js App import should be present once');
459+
}
460+
{
461+
const match = aposContent.match(
462+
/AposAdminBar_[\d]+App\(\);/g
463+
);
464+
assert.equal(match?.length, 1, 'AposAdminBar.js App should be called once');
465+
}
466+
assert.match(
467+
aposContent,
468+
/import AposCommandMenuKey from ".\/@apostrophecms\/command-menu\/apos\/components\/AposCommandMenuKey\.vue";/
419469
);
420-
assert.equal(match?.length, 1, 'AposAdminBar.js App import should be present once');
421-
}
422-
{
423-
const match = aposContent.match(
424-
/AposAdminBar_[\d]+App\(\);/g
470+
assert.match(
471+
aposContent,
472+
/import Link from ".\/@apostrophecms\/rich-text-widget\/apos\/tiptap-extensions\/Link\.js";/
425473
);
426-
assert.equal(match?.length, 1, 'AposAdminBar.js App should be called once');
427474
}
428-
assert.match(
429-
aposContent,
430-
/import AposCommandMenuKey from "[^"]+\/@apostrophecms\/command-menu\/ui\/apos\/components\/AposCommandMenuKey\.vue";/
431-
);
432-
assert.match(
433-
aposContent,
434-
/import Link from "[^"]+\/@apostrophecms\/rich-text-widget\/ui\/apos\/tiptap-extensions\/Link\.js";/
435-
);
436-
437475
// III. Extra Build Entrypoints & Rebundle Modules
438476
const articleEntryContent = await fs.readFile(
439477
path.join(rootDirSrc, 'article.js'),
440478
'utf8'
441479
);
442480

443-
assert(articleEntryContent.includes('article-page/ui/src/main.scss'));
444-
assert(articleEntryContent.includes('article-page/ui/src/index.js'));
445-
assert(articleEntryContent.includes('article-page/ui/src/main.js'));
446-
447-
const toolsEntryContent = await fs.readFile(
448-
path.join(rootDirSrc, 'tools.js'),
449-
'utf8'
450-
);
451-
452-
assert(toolsEntryContent.includes('selected-article-widget/ui/src/tabs.js'));
453-
454-
{
455-
const match = srcContent.match(
456-
/import topic_\d+App from "[^"]+\/@apostrophecms\/home-page\/ui\/src\/topic\.js";/g
457-
);
458-
assert.equal(match?.length, 1, 'home-page topic.js should be imported once');
459-
}
460-
{
461-
const match = srcContent.match(
462-
/import main_\d+App from "[^"]+\/@apostrophecms\/home-page\/ui\/src\/main\.js";/g
463-
);
464-
assert.equal(match?.length, 1, 'home-page main.js should be imported once');
465-
}
466-
{
467-
const match = srcContent.match(
468-
/import topic_\d+App from "[^"]+\/article-widget\/ui\/src\/topic\.js";/g
481+
if (process.platform === 'win32') {
482+
assert(articleEntryContent.includes('article-page/ui/src/main.scss'));
483+
assert(articleEntryContent.includes('article-page/ui/src/index.js'));
484+
assert(articleEntryContent.includes('article-page/ui/src/main.js'));
485+
const toolsEntryContent = await fs.readFile(
486+
path.join(rootDirSrc, 'tools.js'),
487+
'utf8'
469488
);
470-
assert.equal(match?.length, 1, 'article-widget topic.js should be imported once');
471-
}
472-
{
473-
const match = srcContent.match(
474-
/import carousel_\d+App from "[^"]+\/article-widget\/ui\/src\/carousel\.js";/g
489+
assert(toolsEntryContent.includes('selected-article-widget/ui/src/tabs.js'));
490+
{
491+
const match = srcContent.match(
492+
/import topic_\d+App from "[^"]+\/@apostrophecms\/home-page\/ui\/src\/topic\.js";/g
493+
);
494+
assert.equal(match?.length, 1, 'home-page topic.js should be imported once');
495+
}
496+
{
497+
const match = srcContent.match(
498+
/import main_\d+App from "[^"]+\/@apostrophecms\/home-page\/ui\/src\/main\.js";/g
499+
);
500+
assert.equal(match?.length, 1, 'home-page main.js should be imported once');
501+
}
502+
{
503+
const match = srcContent.match(
504+
/import topic_\d+App from "[^"]+\/article-widget\/ui\/src\/topic\.js";/g
505+
);
506+
assert.equal(match?.length, 1, 'article-widget topic.js should be imported once');
507+
}
508+
{
509+
const match = srcContent.match(
510+
/import carousel_\d+App from "[^"]+\/article-widget\/ui\/src\/carousel\.js";/g
511+
);
512+
assert.equal(match?.length, 1, 'article-widget carousel.js should be imported once');
513+
}
514+
} else {
515+
assert(articleEntryContent.includes('article-page/src/main.scss'));
516+
assert(articleEntryContent.includes('article-page/src/index.js'));
517+
assert(articleEntryContent.includes('article-page/src/main.js'));
518+
const toolsEntryContent = await fs.readFile(
519+
path.join(rootDirSrc, 'tools.js'),
520+
'utf8'
475521
);
476-
assert.equal(match?.length, 1, 'article-widget carousel.js should be imported once');
522+
assert(toolsEntryContent.includes('selected-article-widget/src/tabs.js'));
523+
{
524+
const match = srcContent.match(
525+
/import topic_\d+App from ".\/@apostrophecms\/home-page\/src\/topic\.js";/g
526+
);
527+
assert.equal(match?.length, 1, 'home-page topic.js should be imported once');
528+
}
529+
{
530+
const match = srcContent.match(
531+
/import main_\d+App from ".\/@apostrophecms\/home-page\/src\/main\.js";/g
532+
);
533+
assert.equal(match?.length, 1, 'home-page main.js should be imported once');
534+
}
535+
{
536+
const match = srcContent.match(
537+
/import topic_\d+App from ".\/article-widget\/src\/topic\.js";/g
538+
);
539+
assert.equal(match?.length, 1, 'article-widget topic.js should be imported once');
540+
}
541+
{
542+
const match = srcContent.match(
543+
/import carousel_\d+App from ".\/article-widget\/src\/carousel\.js";/g
544+
);
545+
assert.equal(match?.length, 1, 'article-widget carousel.js should be imported once');
546+
}
477547
}
478548
});
479549

0 commit comments

Comments
 (0)