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
395 changes: 394 additions & 1 deletion deno.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions examples/import_map.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"imports": {
"react/jsx-dev-runtime": "",
"react/jsx-runtime": ""
}
}
5 changes: 4 additions & 1 deletion examples/vite.config.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import reactImport from 'npm:@vitejs/plugin-react@2.2.0';
import { defineConfig } from 'npm:vite@3.2.4';
import denoResolve from '../mod.ts';

const react = reactImport as never as typeof reactImport.default;

export default defineConfig({
plugins: [denoResolve()],
plugins: [react(), denoResolve()],
});
21 changes: 20 additions & 1 deletion src/deno.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module, ModuleInfo, PluginConfig } from './types.ts';
import { CacheInfo, Module, ModuleInfo, PluginConfig } from './types.ts';

export function createDeno(
{ cacheCache, infoCache, moduleCache, tempDirectory }: PluginConfig,
Expand Down Expand Up @@ -77,9 +77,28 @@ export function createDeno(
return moduleInfo;
}

async function cacheInfo(): Promise<CacheInfo> {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this cacheDirs?

const p = Deno.run({
cmd: [Deno.execPath(), 'info', '--json'],
stdout: 'piped',
stderr: 'piped',
cwd: tempDirectory,
});

const status = await p.status();
if (!status.success) {
throw new Error(`invariant: could not get cache info`);
}

const output = await p.output();
const cacheInfo: CacheInfo = JSON.parse(new TextDecoder().decode(output));
return cacheInfo;
}

return {
cache,
info,
module,
cacheInfo,
};
}
135 changes: 131 additions & 4 deletions src/npm-resolve.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,140 @@
import { createDeno } from './deno.ts';
import { PluginConfig } from './types.ts';

export default function npmResolve({}: PluginConfig) {
return {
function createPlugin(plugin: import('npm:vite').Plugin) {
return plugin;
}

export default function npmResolve(config: PluginConfig) {
// TODO(bartlomieju): both plugins should use the same instance, or maybe
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to create two separate ones. The cache instances are passed in from the plugin config in mod.ts.

// both plugins should be combined.
const deno = createDeno(config);

return createPlugin({
name: 'vite:deno-npm-resolve',

enforce: 'pre' as const,

resolveId() {
return null;
configResolved(config) {
const previousCreateResolver = config.createResolver;
const createResolver: typeof config['createResolver'] = (options) => {
return previousCreateResolver(options);
};

config.createResolver = createResolver;
},

async resolveId(id: string, importer: string | undefined) {
// console.log('resolveId npm', id, importer);

if (id.startsWith('npm:')) {
await deno.cache(id);
return id;
} else {
const r = await this.resolve(id, importer, { skipSelf: true });
console.log(`resolve result id: ${id} importer: ${importer} r:`, r);
return r;
}
},

async load(id: string) {
console.log('load npm', id);

if (!id.startsWith('npm:')) {
return null;
}

const ref = npmPackageReference(id);

// TODO(bartlomieju): this shouldn't be called on each load, it can
// be cached per run
const cacheInfo = await deno.cacheInfo();
const npmCache = cacheInfo.npmCache;

const specifier = ref.name;
const version = ref.versionReq;

if (!version) {
throw new Error(`Version not specified for ${specifier}`);
}

console.log('NpmPackageReference', ref);
const moduleDirPath =
`${npmCache}/registry.npmjs.org/${specifier}/${version}`;

const packageJson = JSON.parse(
await Deno.readTextFile(
`${moduleDirPath}/package.json`,
),
);

let file = packageJson.main || 'index.js';
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to pass the package dir here to this.resolve() so the node resolve plugin picks it up and does the work, no?

(Is that what Deno would do internally, basically? Does Deno internally handle exports/main/module/files/folders for entrypoints, etc?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try, if this.resolve() would work then it would be great. Yes, Deno handles this internally

if (ref.subPath) {
// TODO(bartlomieju): handle other extensions
file = `${ref.subPath}.js`;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only one facet of multiple ways a package entrypoint can be defined. If Deno does this internally (I assume it would have to?) perhaps we just focus on getting the Deno side fixed? Else it's a lot of rework here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but it will be slow - calling a subprocess to resolve each specifier seems worse than porting some code/using a package that can do that.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point 😄

}
return await Deno.readTextFile(`${moduleDirPath}/${file}`);
},
});
}

interface NpmPackageReference {
name: string;
versionReq: string | null;
subPath: string | null;
}

function npmPackageReference(specifier: string): NpmPackageReference {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great function

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a direct port from Deno's code, that's how it's handled internally by Deno

if (!specifier.startsWith('npm:')) {
throw new Error(`Invalid npm package reference: ${specifier}`);
}

specifier = specifier.substring(4);
const parts = specifier.split('/');
let namePartLen;
if (specifier.startsWith('@')) {
namePartLen = 2;
} else {
namePartLen = 1;
}
if (parts.length < namePartLen) {
throw new Error(`Invalid npm package reference: ${specifier}`);
}
const nameParts = parts.slice(0, namePartLen);
let lastNamePart = nameParts[nameParts.length - 1];
const atIndex = lastNamePart.lastIndexOf('@');
let version;
let name;

if (atIndex !== -1) {
version = lastNamePart.substring(atIndex + 1);
lastNamePart = lastNamePart.substring(0, atIndex);

if (namePartLen === 1) {
name = lastNamePart;
} else {
name = `${nameParts[0]}/${lastNamePart}`;
}
} else {
name = nameParts.join('/');
}

let subPath = null;
if (parts.length !== namePartLen) {
subPath = parts.slice(namePartLen).join('/');
}

// TODO(bartlomieju): handle version specified after subpath

if (!name) {
throw new Error(
`Invalid npm package reference: ${specifier}. Did not contain a package name`,
);
}

return {
name,
versionReq: version || null,
subPath,
};
}
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ export interface ModuleInfo {
roots: string[];
}

export interface CacheInfo {
denoDir: string;
modulesCache: string;
npmCache: string;
typescriptCache: string;
registryCache: string;
originStorage: string;
}

export interface PluginConfig {
cacheCache: CacheCache;
infoCache: InfoCache;
Expand Down