diff --git a/__tests__/code-gen.spec.ts b/__tests__/code-gen.spec.ts index 13ab0b8..87b669b 100644 --- a/__tests__/code-gen.spec.ts +++ b/__tests__/code-gen.spec.ts @@ -1,6 +1,6 @@ import test from 'ava' import { parse, traverse } from '@babel/core' -import { createCodeGenerator } from '../dist/code-gen' +import { createCodeGenerator } from '../src/code-gen' import { createScanner } from '../dist/scanner' import type { ModuleInfo } from '../src/interface' diff --git a/__tests__/inject.spec.ts b/__tests__/inject.spec.ts index 59db734..5a457e6 100644 --- a/__tests__/inject.spec.ts +++ b/__tests__/inject.spec.ts @@ -1,8 +1,8 @@ import test from 'ava' -import { len } from '../dist/shared' -import { createInjectScript } from '../dist/inject' -import { jsdelivr } from '../dist/url' -import type { TrackModule } from '../dist' +import { len } from '../src/shared' +import { createInjectScript } from '../src/inject' +import { jsdelivr } from '../src/url' +import type { TrackModule } from '../src' interface MockIIFEMdoule extends TrackModule{ relativeModule: string diff --git a/__tests__/shared.spec.ts b/__tests__/shared.spec.ts index 33be710..167c182 100644 --- a/__tests__/shared.spec.ts +++ b/__tests__/shared.spec.ts @@ -1,6 +1,6 @@ import fsp from 'fs/promises' import test from 'ava' -import { len, lookup } from '../dist/shared' +import { len, lookup } from '../src/shared' test('len', (t) => { diff --git a/__tests__/vm.spec.ts b/__tests__/vm.spec.ts new file mode 100644 index 0000000..fe6b8aa --- /dev/null +++ b/__tests__/vm.spec.ts @@ -0,0 +1,32 @@ +import test from 'ava' +import { MAX_CONCURRENT, createConcurrentQueue, createVM } from '../src/vm' + +test('native vm', async (t) => { + const vm = createVM() + await vm.run('var nonzzz = \'test plugin\'', { name: 'nonzzz' } as any, (err) => err) + t.is(vm.bindings.has('nonzzz'), true) +}) + +test('shadow vm', async (t) => { + const vm = createVM() + await vm.run('window.nonzzz = 123', { name: 'nonzzz' } as any, (err) => err) + t.is(vm.bindings.has('nonzzz'), true) +}) + +test('throw error in vm', async (t) => { + const vm = createVM() + await vm.run('throw new Error(\'error\')', { name: 'nonzzz' } as any, (err) => { + t.is(err?.message, 'nonzzz') + }) +}) + +test('task queue', async (t) => { + const tasks = [() => Promise.resolve(), () => Promise.resolve(), () => Promise.reject(new Error('3'))] + + const queue = createConcurrentQueue(MAX_CONCURRENT) + for (const task of tasks) { + queue.enqueue(task) + } + const err = await t.throwsAsync(queue.wait()) + t.is(err?.message, '3') +}) diff --git a/docs/URL.md b/docs/URL.md index 299bddc..cf780f4 100644 --- a/docs/URL.md +++ b/docs/URL.md @@ -4,7 +4,7 @@ ```js import { cdn } from 'vite-plugin-cdn2' -import { unpkg } from 'vite-plugin-cdn2/url' +import { unpkg } from 'vite-plugin-cdn2/url.js' cdn({url:unpkg,modules:['vue']}) diff --git a/example/package.json b/example/package.json index 9542b90..32a3132 100644 --- a/example/package.json +++ b/example/package.json @@ -1,7 +1,7 @@ { "name": "vite-plugin-cdn2-example", "scripts": { - "dev": "vite", + "dev": "set DEBUG=vite-plugin-cdn2 & vite", "build": "vite build", "preview": "vite preview" }, diff --git a/example/vite.config.js b/example/vite.config.js index 58696d6..005695a 100644 --- a/example/vite.config.js +++ b/example/vite.config.js @@ -4,7 +4,7 @@ import vue from '@vitejs/plugin-vue' import { VarletUIResolver } from 'unplugin-vue-components/resolvers' import Components from 'unplugin-vue-components/vite' import { cdn } from 'vite-plugin-cdn2' -import { unpkg } from 'vite-plugin-cdn2/url' +import { unpkg } from 'vite-plugin-cdn2/url.js' import { compression } from 'vite-plugin-compression2' import Inspect from 'vite-plugin-inspect' diff --git a/package.json b/package.json index a16afd5..4eff996 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "import": "./dist/index.mjs", "require": "./dist/index.js" }, - "./url": { - "types":"./dist/url.d.ts", + "./url.js": { + "types": "./dist/url.d.ts", "import": "./dist/url.mjs", "require": "./dist/url.js" }, @@ -39,8 +39,8 @@ }, "typesVersions": { "*": { - "*": [ - "./dist/*" + "url.js": [ + "./dist/url.d.ts" ] } }, @@ -57,6 +57,7 @@ "devDependencies": { "@rollup/plugin-json": "^6.0.0", "@types/babel__core": "^7.20.1", + "@types/debug": "^4.1.8", "ava": "^5.2.0", "c8": "^7.12.0", "eslint": "^8.23.1", @@ -72,6 +73,7 @@ "@babel/core": "^7.22.5", "@rollup/pluginutils": "^5.0.2", "@types/estree": "^1.0.1", + "debug": "^4.3.4", "happy-dom": "^6.0.4", "rs-module-lexer": "^1.0.0" }, diff --git a/src/code-gen.ts b/src/code-gen.ts index 9013de5..9245bef 100644 --- a/src/code-gen.ts +++ b/src/code-gen.ts @@ -22,7 +22,7 @@ export class CodeGen { const modules = Array.from(new Set([...imports.map(i => i.n)])) for (const m of modules) { if (this.dependencies.has(m)) return true - return false + continue } return false } diff --git a/src/index.ts b/src/index.ts index 4a5da6b..8797f5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { createFilter } from '@rollup/pluginutils' import type { Plugin } from 'vite' +import _debug from 'debug' import { createScanner } from './scanner' import { createInjectScript } from './inject' import { createCodeGenerator } from './code-gen' @@ -7,6 +8,8 @@ import { isSupportThreads } from './shared' import { jsdelivr } from './url' import type { CDNPluginOptions } from './interface' +const debug = _debug('vite-plugin-cdn2') + function cdn(opts: CDNPluginOptions = {}): Plugin { const { modules = [], url = jsdelivr, include = /\.(mjs|js|ts|vue|jsx|tsx)(\?.*|)$/, exclude, logLevel = 'warn', resolve: resolver } = opts const filter = createFilter(include, exclude) @@ -20,7 +23,9 @@ function cdn(opts: CDNPluginOptions = {}): Plugin { const [isSupport, version] = isSupportThreads() try { if (!isSupport) throw new Error(`vite-plugin-cdn2 can't work with nodejs ${version}.`) + debug('start scanning') await scanner.scanAllDependencies() + debug('scanning done', scanner.dependencies) generator.injectDependencies(scanner.dependencies) if (logLevel === 'warn') { scanner.failedModules.forEach((errorMessage, name) => config.logger.error(`vite-plugin-cdn2: ${name} ${errorMessage ? errorMessage : 'resolved failed.Please check it.'}`)) diff --git a/src/scanner.ts b/src/scanner.ts index ece3d1f..c047e1c 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -2,7 +2,7 @@ import fsp from 'fs/promises' import worker_threads from 'worker_threads' import type { MessagePort } from 'worker_threads' import { MAX_CONCURRENT, createConcurrentQueue, createVM } from './vm' -import { is, len, lookup } from './shared' +import { _import, is, len, lookup } from './shared' import type { IIFEModuleInfo, IModule, ModuleInfo, ResolverFunction, TrackModule } from './interface' // This file is a simply dependencies scanner. @@ -10,6 +10,10 @@ import type { IIFEModuleInfo, IModule, ModuleInfo, ResolverFunction, TrackModule // we consume all expection modules in the plugin itself. // Notice. This file don't handle any logic with script inject. +// TODO +// We pack this file just to make the test pass. If we migrate to other test framework +// Don't forget remove it. + interface WorkerData { scannerModule: IModule[] workerPort: MessagePort @@ -83,7 +87,7 @@ async function tryResolveModule( const code = await fsp.readFile(iifeFilePath, 'utf8') Object.assign(meta, { name, version, code, relativeModule: iifeRelativePath, ...rest }) } - const pkg = await import(moduleName) + const pkg = await _import(moduleName) const keys = Object.keys(pkg) // If it's only exports by default if (keys.includes('default') && len(keys) !== 1) { diff --git a/src/shared.ts b/src/shared.ts index 80a5d1d..2f316e8 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -30,3 +30,7 @@ export function is(condit: boolean, message: string) { throw new Error(message) } } + +// TODO +// If we find the correct dynamic import handing it should be removed. +export const _import = new Function('specifier', 'return import(specifier)') diff --git a/src/vm.ts b/src/vm.ts index 78c570f..12f2789 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -4,50 +4,51 @@ import { Window } from 'happy-dom' import { len } from './shared' import type { ModuleInfo } from './interface' +// Normal. Each umd of iife library only export one module. But some libraries don't +// follow this principle. They export some variable starting with __ like `Vue`,`Element-Plus` +// Variables staring with an underscore are private variables by convention and we shouldn't +// parse them. If they are mulitple variables then we only take the last. + export function createVM() { const bindings: Map = new Map() const window = new Window() const context = vm.createContext({}) - let _meta: ModuleInfo = null - let id = 0 - let callerId = 0 const updateBindings = (name: string, meta: ModuleInfo) => { bindings.set(meta.name, { ...meta, global: name }) } const shadow = new Proxy(window, { set(target, key: string, value, receiver) { - callerId++ - if (id === callerId) updateBindings(key, _meta) Reflect.set(target, key, value, receiver) return true } }) const run = (code: string, meta: ModuleInfo, handler: (err: Error)=> void) => { - _meta = meta try { vm.runInContext(code, context) - // TODO - // This is a temporary solution. - // when vm run script it can't run others logic in threads it will directly - // end the function. - // So we need to get the last one using the tag. // https://github.com/nodejs/help/issues/1378 - id = len(Object.keys(context)) + const c = Object.keys(context) Object.assign(shadow, context) + let last = c.pop() + while (last.startsWith('__')) { + last = c.pop() + } + updateBindings(last, meta) // context free for (const key in context) { Reflect.deleteProperty(context, key) } - _meta = null - callerId = 0 - id = 0 } catch (error) { try { // In most cases there will only be one variable window.eval(code) - updateBindings(Object.keys(shadow).pop(), meta) + const c = Object.keys(shadow) + let last = c.pop() + while (last.startsWith('__')) { + last = c.pop() + } + updateBindings(last, meta) } catch (_) { handler(new Error(meta.name)) } diff --git a/tsup.config.ts b/tsup.config.ts index 1e75ec2..c16740d 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import type { Options } from 'tsup' export const tsup: Options = { - entry: ['src/*.ts'], + entry: ['src/index.ts', 'src/url.ts', 'src/scanner.ts'], format: ['cjs', 'esm'], dts: true, splitting: true, diff --git a/yarn.lock b/yarn.lock index 8f76759..6c13d81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -531,6 +531,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.1.8": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" + integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ== + dependencies: + "@types/ms" "*" + "@types/estree@^1.0.0", "@types/estree@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" @@ -553,6 +560,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/node@*": version "18.15.11" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"