Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/query-before-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

Compile modules whose id has the extension after the query, such as unplugin-icons' `~icons/mdi/folder?width=2em.tsx`. These were left to Vite's default JSX transform, which produced React code (#206). Custom `extensions` are now also matched when the query contains a dot.
22 changes: 19 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,18 @@ export interface RefreshOptions {

function getExtension(filename: string): string {
const index = filename.lastIndexOf('.');
return index < 0 ? '' : filename.substring(index).replace(/\?.+$/, '');
return index < 0 ? '' : filename.substring(index);
}

/**
* The path the transform compiles for `id`. This is normally `id` without its
* query. Some plugins put the extension after the query instead, such as
* unplugin-icons' `~icons/mdi/folder?width=2em.tsx`. Then the full id is used,
* as it was before queries were stripped. #206
*/
function getTransformPath(id: string, isHandled: (path: string) => boolean): string {
const cleanId = id.replace(/\?.*$/, '');
return cleanId !== id && !isHandled(cleanId) && isHandled(id) ? id : cleanId;
}
// The packages whose dev/production server builds are selected by the
// `development` export condition. A dependency on either means the package
Expand Down Expand Up @@ -2152,7 +2163,6 @@ export default manifest;`;
async transform(source, id, transformOptions) {
if (isTsrxCssModule(id)) return null;
const isSsr = getEnvironmentConsumer(this.environment, transformOptions) === 'server';
const currentFileExtension = getExtension(id);

const extensionsToWatch = options.extensions || [];
const allExtensions = extensionsToWatch.map((extension) =>
Expand All @@ -2168,7 +2178,13 @@ export default manifest;`;
// manifest key / dev URL); keep it for the `$$moduleUrl` injection
// while the transform pipeline below works on the clean file path.
const moduleId = id;
id = id.replace(/\?.*$/, '');
id = getTransformPath(
id,
(path) => /\.[mc]?[tj]sx$/i.test(path) || allExtensions.includes(getExtension(path)),
);
// Read the extension from the query-free path, so a dot inside the
// query (`a.mdx?x=1.5`) is not taken as the extension.
const currentFileExtension = getExtension(id);
const isTsrx = isTsrxModule(id);
const inNodeModules = /node_modules/.test(id);
// Primitive names stop at node_modules: a dependency's internals
Expand Down
68 changes: 68 additions & 0 deletions src/transform-ids.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createServer, type Plugin, type ViteDevServer } from 'vite';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import solidPlugin from './index.js';

let root: string;
let server: ViteDevServer;

// Resolves ids the way unplugin-icons does: the extension is appended after
// the query, giving `~icons/mdi/folder?width=2em.tsx`.
const iconsPlugin: Plugin = {
name: 'test-icons',
enforce: 'pre',
resolveId(id) {
if (!id.startsWith('~icons/')) return null;
return id.endsWith('.tsx') ? id : `${id}.tsx`;
},
load(id) {
if (id.startsWith('~icons/')) {
return 'export default (props = {}) => <svg viewBox="0 0 24 24" {...props} />;';
}
},
};

beforeAll(async () => {
root = await mkdtemp(path.join(tmpdir(), 'solid-transform-ids-'));
// The modules import @solidjs/web from this repo's install.
await symlink(
fileURLToPath(new URL('../node_modules', import.meta.url)),
path.join(root, 'node_modules'),
'dir',
);
await mkdir(path.join(root, 'src'));
await writeFile(path.join(root, 'src/Comp.custom'), 'export const Comp = () => <div>custom</div>;\n');
server = await createServer({
root,
configFile: false,
appType: 'custom',
logLevel: 'silent',
server: { middlewareMode: true, hmr: false, watch: null },
optimizeDeps: { noDiscovery: true, include: [] },
plugins: [solidPlugin({ extensions: ['.custom'] }), iconsPlugin],
});
});

afterAll(async () => {
await server?.close();
await rm(root, { recursive: true, force: true });
});

describe('transform ids with queries', () => {
// #206: since queries were stripped first, this id lost its extension and
// Vite compiled the JSX for React instead.
it('compiles JSX when the extension follows the query', async () => {
const result = await server.environments.client.transformRequest('~icons/mdi/folder?width=2em');
expect(result?.code).toContain('_$template(');
expect(result?.code).not.toMatch(/react/i);
});

it('reads a custom extension from the path, not from a dot in the query', async () => {
const result = await server.environments.client.transformRequest('/src/Comp.custom?x=1.5');
expect(result?.code).toContain('_$template(');
expect(result?.code).not.toContain('<div>custom</div>');
});
});
Loading