Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions packages/root/src/core/hooks/useAssetMap.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {renderToString} from 'preact-render-to-string';
import {expect, test} from 'vitest';
import {renderJsxToString} from '../../jsx/jsx-render.js';
import type {Asset, AssetMap} from '../../render/asset-map/asset-map.js';
import {
ASSET_MAP_CONTEXT,
useAsset,
useAssetMap,
useAssetUrl,
} from './useAssetMap.js';

/**
* Returns a stub asset map backed by a `src` -> `assetUrl` mapping, mimicking
* the lookups performed by `BuildAssetMap` and `DevServerAssetMap`.
*/
function testAssetMap(assetUrls: Record<string, string>): AssetMap {
return {
get: (src: string): Asset | null => {
if (!(src in assetUrls)) {
return null;
}
return {
src: src,
assetUrl: assetUrls[src],
getCssDeps: async () => [],
getJsDeps: async () => [],
getModulePreloadDeps: async () => [],
};
},
};
}

const assetMap = testAssetMap({
'bundles/main.ts': '/assets/main.abcd1234.js',
// Route files are in the manifest for their CSS deps, but have no asset URL.
'routes/index.tsx': '',
});

/**
* The renderer picks between `preact-render-to-string` and `@blinkk/root/jsx`
* depending on the `jsxRenderer` config, so the hook is verified against both.
*/
const renderers: Array<[string, (vnode: any) => string]> = [
['preact-render-to-string', (vnode) => renderToString(vnode)],
['@blinkk/root/jsx', (vnode) => renderJsxToString(vnode, {mode: 'minimal'})],
];

/** Renders a component with the asset map provided by the renderer. */
function render(
vnode: any,
options?: {assetMap?: AssetMap; renderJsx?: (vnode: any) => string}
) {
const renderJsx = options?.renderJsx || renderers[0][1];
if (!options?.assetMap) {
return renderJsx(vnode);
}
return renderJsx(
<ASSET_MAP_CONTEXT.Provider value={options.assetMap}>
{vnode}
</ASSET_MAP_CONTEXT.Provider>
);
}

test.each(renderers)(
'useAssetUrl() returns the compiled asset url (%s)',
(_name, renderJsx) => {
function Page() {
return <div data-main={useAssetUrl('bundles/main.ts')} />;
}
expect(render(<Page />, {assetMap, renderJsx})).toBe(
'<div data-main="/assets/main.abcd1234.js"></div>'
);
}
);

test('useAssetUrl() accepts a leading slash', () => {
function Page() {
return <div data-main={useAssetUrl('/bundles/main.ts')} />;
}
expect(render(<Page />, {assetMap})).toBe(
'<div data-main="/assets/main.abcd1234.js"></div>'
);
});

test('useAssetUrl() throws when the src is not in the asset map', () => {
function Page() {
return <div data-main={useAssetUrl('/bundles/missing.ts')} />;
}
expect(() => render(<Page />, {assetMap})).toThrow(
'asset not found: /bundles/missing.ts'
);
});

test('useAssetUrl() throws when the asset has no serving url', () => {
function Page() {
return <div data-main={useAssetUrl('routes/index.tsx')} />;
}
expect(() => render(<Page />, {assetMap})).toThrow(
'asset not found: routes/index.tsx'
);
});

test('useAsset() returns the asset from the module graph', () => {
let asset: Asset | null = null;
function Page() {
asset = useAsset('/bundles/main.ts');
return <div />;
}
render(<Page />, {assetMap});
expect(asset!.src).toBe('bundles/main.ts');
expect(asset!.assetUrl).toBe('/assets/main.abcd1234.js');
});

test('useAssetMap() throws when rendered outside of the provider', () => {
function Page() {
useAssetMap();
return <div />;
}
expect(() => render(<Page />)).toThrow('ASSET_MAP_CONTEXT not found');
});
70 changes: 70 additions & 0 deletions packages/root/src/render/asset-map/dev-asset-map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type {ModuleGraph} from 'vite';
import {afterEach, beforeEach, expect, test, vi} from 'vitest';
import {RootConfig} from '../../core/config.js';
import {DevServerAssetMap} from './dev-asset-map.js';

let workspaceDir: string;
let rootDir: string;
let assetMap: DevServerAssetMap;

// A module graph that never resolves a file, which exercises the fallback
// branches that serve files that never made it into vite's module graph.
const emptyModuleGraph = {
getModulesByFile: () => undefined,
} as unknown as ModuleGraph;

beforeEach(() => {
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'root-dev-assets-'));
// `searchForWorkspaceRoot()` walks up for a `pnpm-workspace.yaml`, so the
// workspace root is deterministic within the temp dir.
fs.writeFileSync(path.join(workspaceDir, 'pnpm-workspace.yaml'), '');
rootDir = path.join(workspaceDir, 'site');
fs.mkdirSync(rootDir);
assetMap = new DevServerAssetMap({rootDir} as RootConfig, emptyModuleGraph);
});

afterEach(() => {
if (workspaceDir) {
fs.rmSync(workspaceDir, {recursive: true, force: true});
}
});

test('serves files within the project from the root dir', () => {
const asset = assetMap.get('bundles/main.ts');
expect(asset!.assetUrl).toBe('/bundles/main.ts');
});

test('serves sibling dirs sharing the root dir prefix from /@fs/', () => {
// `<workspace>/site-legacy` is a sibling of `<workspace>/site`, not a child
// of it, even though its path starts with the root dir's path.
const filePath = path.join(workspaceDir, 'site-legacy/main.ts');
fs.mkdirSync(path.dirname(filePath));
fs.writeFileSync(filePath, 'export default 1;\n');

const asset = assetMap.get('../site-legacy/main.ts');
expect(asset!.assetUrl).toBe(`/@fs/${filePath}`);
});

test('serves workspace files that do not exist on disk', () => {
const filePath = path.join(workspaceDir, 'packages/ui/main.ts');
const asset = assetMap.get('../packages/ui/main.ts');
expect(asset!.assetUrl).toBe(`/@fs/${filePath}`);
});

test('returns null for files outside of the workspace', () => {
// The missing-asset lookup logs via console.log; stub it so the test output
// stays clean, and assert it fired.
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
try {
const src = path.relative(rootDir, path.join(os.tmpdir(), 'outside.ts'));
expect(assetMap.get(src)).toBeNull();
expect(logSpy).toHaveBeenCalledWith(
`could not find asset in asset map: ${src}`
);
} finally {
logSpy.mockRestore();
}
});