& {
11 | run(
12 | task: [P, ...Parameters>],
13 | options?: Parameters[1]
14 | ): Promise>, void>>>
15 | }
16 |
17 | type Fn = (...args: any[]) => any
18 |
--------------------------------------------------------------------------------
/src/utils/node/toDebugPath.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import { relativeToCwd } from './relativeToCwd'
3 |
4 | export function toDebugPath(file: string) {
5 | return fs.existsSync(file.replace(/[#?].*$/, '')) ? relativeToCwd(file) : file
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/noop.ts:
--------------------------------------------------------------------------------
1 | export function noop() {}
2 |
--------------------------------------------------------------------------------
/src/utils/objectHash.ts:
--------------------------------------------------------------------------------
1 | import { murmurHash } from './murmur3'
2 | import { sortObjects } from './sortObjects'
3 |
4 | export function toObjectHash(data: object) {
5 | const json = JSON.stringify(data, sortObjects)
6 | return murmurHash(json)
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/parseLazyImport.ts:
--------------------------------------------------------------------------------
1 | const importRE = /\b\(["']([^"']+)["']\)/
2 |
3 | export const parseLazyImport = (fn: Function) => {
4 | const match = importRE.exec(fn.toString())
5 | return match && match[1]
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/pick.ts:
--------------------------------------------------------------------------------
1 | import { PossibleKeys } from './types'
2 |
3 | export function pick<
4 | T extends object,
5 | P extends ReadonlyArray>
6 | >(
7 | obj: T,
8 | keys: P,
9 | filter: (value: any, key: P[number]) => boolean = () => true
10 | ): Pick {
11 | const picked: any = {}
12 | for (const key of keys) {
13 | const value = obj[key]
14 | if (filter(value, key)) {
15 | picked[key] = value
16 | }
17 | }
18 | return picked
19 | }
20 |
21 | export function pickAllExcept<
22 | T extends object,
23 | P extends ReadonlyArray>
24 | >(obj: T, keys: P) {
25 | return pick(obj, Object.keys(obj) as any, (_, key) => !keys.includes(key))
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/plural.ts:
--------------------------------------------------------------------------------
1 | export function plural(count: number, one: string, many?: string) {
2 | return count + ' ' + (count == 1 ? one : many || one + 's')
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/readJson.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'fs'
2 |
3 | export type Reviver = (this: any, key: string, value: any) => any
4 |
5 | export function readJson(p: string, reviver?: Reviver): T {
6 | return JSON.parse(readFileSync(p, 'utf8'), reviver)
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/reduceSerial.ts:
--------------------------------------------------------------------------------
1 | type Promisable = T | PromiseLike
2 |
3 | export async function reduceSerial(
4 | array: readonly T[],
5 | reducer: (result: U, element: T) => Promisable,
6 | init: U
7 | ): Promise {
8 | let reduced = init
9 | for (const element of array) {
10 | const result = await reducer(reduced, element)
11 | if (result != null) {
12 | reduced = result
13 | }
14 | }
15 | return reduced
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/resolveModules.ts:
--------------------------------------------------------------------------------
1 | import { unwrapDefault } from './unwrapDefault'
2 |
3 | export async function resolveModules[]>(
4 | ...modules: T
5 | ): Promise<{
6 | [Index in keyof T]: Awaited extends infer Resolved
7 | ? Resolved extends { default: infer DefaultExport }
8 | ? DefaultExport
9 | : Resolved
10 | : never
11 | }> {
12 | return (await Promise.all(modules)).map(unwrapDefault) as any
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/rollupTypes.ts:
--------------------------------------------------------------------------------
1 | type PartialNull = {
2 | [P in keyof T]: T[P] | null
3 | }
4 |
5 | interface ModuleOptions {
6 | meta: Record
7 | moduleSideEffects: boolean | 'no-treeshake'
8 | syntheticNamedExports: boolean | string
9 | }
10 |
11 | export interface PartialResolvedId extends Partial> {
12 | external?: boolean | 'absolute' | 'relative'
13 | id: string
14 | }
15 |
16 | export interface SourceDescription extends Partial> {
17 | ast?: unknown /* AcornNode */
18 | code: string
19 | map?: unknown /* SourceMapInput */
20 | }
21 |
22 | export type TransformResult = string | null | void | Partial
23 |
24 | export type ResolveIdHook = (
25 | id: string,
26 | importer?: string | null
27 | ) => Promise
28 |
--------------------------------------------------------------------------------
/src/utils/sortObjects.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Meant to be used with `JSON.stringify` to ensure that
3 | * object keys have a consistent order.
4 | */
5 | export function sortObjects(_key: string, value: any) {
6 | if (value && value.constructor == Object) {
7 | const copy: any = {}
8 | for (const key of Object.keys(value).sort()) {
9 | copy[key] = value[key]
10 | }
11 | return copy
12 | }
13 | return value
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/stripHtmlSuffix.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import { stripHtmlSuffix } from './stripHtmlSuffix'
3 |
4 | test('stripHtmlSuffix', () => {
5 | const cases = {
6 | '': '',
7 | '/': '/',
8 | 'index.html': '/',
9 | 'foo.html': '/foo',
10 | '/index.html': '/',
11 | '/foo.html': '/foo',
12 | '/?a=b': '/?a=b',
13 | '/index.html?a=b': '/?a=b',
14 | '/foo.html?a=b': '/foo?a=b',
15 | }
16 |
17 | for (const [input, output] of Object.entries(cases)) {
18 | expect({ input, output: stripHtmlSuffix(input) }).toEqual({ input, output })
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/src/utils/stripHtmlSuffix.ts:
--------------------------------------------------------------------------------
1 | const htmlExtensionRE = /\.html(\?|$)/
2 | const indexHtmlSuffixRE = /\/index.html(\?|$)/
3 |
4 | export function stripHtmlSuffix(url: string) {
5 | if (!url) {
6 | return url
7 | }
8 | if (url[0] !== '/') {
9 | url = '/' + url
10 | }
11 | if (indexHtmlSuffixRE.test(url)) {
12 | return url.replace(indexHtmlSuffixRE, '/$1')
13 | }
14 | return url.replace(htmlExtensionRE, '$1')
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/take.ts:
--------------------------------------------------------------------------------
1 | export function take(map: Map, key: K): V | undefined {
2 | const value = map.get(key)
3 | map.delete(key)
4 | return value
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/textExtensions.ts:
--------------------------------------------------------------------------------
1 | export const textExtensions = /\.(js|css|svg|json|md|rss|html|xml|txt)$/
2 |
--------------------------------------------------------------------------------
/src/utils/throttle.ts:
--------------------------------------------------------------------------------
1 | export function throttle(run: (cb: () => void) => void) {
2 | let throttled = false
3 | return (cb: () => void) => {
4 | if (!throttled) {
5 | throttled = true
6 | run(() => {
7 | throttled = false
8 | cb()
9 | })
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": ["**/*.ts"],
4 | "exclude": ["**/*.spec.ts", "dist", "tsup.config.ts"],
5 | "compilerOptions": {
6 | "composite": false,
7 | "declaration": true,
8 | "declarationMap": true,
9 | "incremental": true,
10 | "moduleResolution": "node",
11 | "outDir": "dist",
12 | "tsBuildInfoFile": "dist/.tsbuildinfo"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions'
2 | import { defineConfig } from 'tsup'
3 |
4 | export default defineConfig({
5 | entry: [
6 | '**/*.ts',
7 | '!**/*.spec.ts',
8 | '!*.config.ts',
9 | '!node_modules/**',
10 | '!dist/**',
11 | ],
12 | outDir: 'dist',
13 | format: ['cjs', 'esm'],
14 | bundle: false,
15 | plugins: [esbuildPluginFilePathExtensions()],
16 | })
17 |
--------------------------------------------------------------------------------
/src/utils/unwrapDefault.ts:
--------------------------------------------------------------------------------
1 | export function unwrapDefault(module: { default: T }): T
2 | export function unwrapDefault(module: Promise): never
3 | export function unwrapDefault(module: object): T
4 | export function unwrapDefault(module: any): T {
5 | const exported = Object.keys(module)
6 | if (exported.length == 1 && exported[0] == 'default') {
7 | return module.default
8 | }
9 | return module
10 | }
11 |
--------------------------------------------------------------------------------
/src/vm/ImporterSet.ts:
--------------------------------------------------------------------------------
1 | import { CompiledModule } from './types'
2 |
3 | export class ImporterSet extends Set {
4 | private _dynamics?: Set
5 |
6 | add(importer: CompiledModule, isDynamic?: boolean) {
7 | if (isDynamic) {
8 | this._dynamics ||= new Set()
9 | this._dynamics.add(importer)
10 | } else {
11 | super.add(importer)
12 | }
13 | return this
14 | }
15 |
16 | delete(importer: CompiledModule) {
17 | const wasStaticImporter = super.delete(importer)
18 | const wasDynamicImporter = !!this._dynamics?.delete(importer)
19 | return wasStaticImporter || wasDynamicImporter
20 | }
21 |
22 | hasDynamic(importer: CompiledModule) {
23 | return !!this._dynamics?.has(importer)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/vm/debug.ts:
--------------------------------------------------------------------------------
1 | import createDebug from 'debug'
2 |
3 | export const debug = createDebug('saus:vm')
4 |
--------------------------------------------------------------------------------
/src/vm/dedupeNodeResolve.ts:
--------------------------------------------------------------------------------
1 | import { bareImportRE } from '@utils/importRegex'
2 | import { join } from 'path'
3 | import { NodeResolveHook } from './hookNodeResolve'
4 |
5 | export function dedupeNodeResolve(
6 | root: string,
7 | dedupe: string[]
8 | ): NodeResolveHook {
9 | const dedupeRE = new RegExp(`^(${dedupe.join('|')})($|/)`)
10 | const dedupeMap: Record = {}
11 |
12 | root = join(root, 'stub.js')
13 | return (id, _importer, nodeResolve) => {
14 | if (bareImportRE.test(id) && dedupeRE.test(id)) {
15 | return (dedupeMap[id] ||= nodeResolve(id, root))
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/vm/dist/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "exports": {
3 | ".": {
4 | "types": "./index.d.ts",
5 | "import": "./index.mjs",
6 | "default": "./index.js"
7 | },
8 | "./*": {
9 | "types": "./*.d.ts",
10 | "import": "./*.mjs",
11 | "default": "./*.js"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/vm/exportNotFound.ts:
--------------------------------------------------------------------------------
1 | let throwOnMissingExport = 0
2 |
3 | export function setThrowOnMissingExport(enabled: boolean) {
4 | throwOnMissingExport += enabled ? 1 : -1
5 | }
6 |
7 | export const exportNotFound = (file: string) =>
8 | new Proxy(Object.prototype, {
9 | get(_, key) {
10 | // Await syntax checks for "then" property to determine
11 | // if this is a promise.
12 | if (throwOnMissingExport > 0 && key !== 'then') {
13 | const err: any = Error(
14 | `The requested module '${file}' does not provide an export named '${
15 | key as string
16 | }'`
17 | )
18 | err.framesToPop = 1
19 | throw err
20 | }
21 | },
22 | })
23 |
--------------------------------------------------------------------------------
/src/vm/forceNodeReload.ts:
--------------------------------------------------------------------------------
1 | import createDebug from 'debug'
2 | import { Module } from 'module'
3 | import { NodeModule } from './nodeModules'
4 |
5 | const debug = createDebug('saus:forceNodeReload')
6 |
7 | type ShouldReloadFn = (id: string, module: NodeModule) => boolean
8 |
9 | export function forceNodeReload(shouldReload: ShouldReloadFn) {
10 | const rawCache = (Module as any)._cache as Record
11 |
12 | // @ts-ignore
13 | Module._cache = new Proxy(rawCache, {
14 | get(_, id: string) {
15 | const cached = rawCache[id]
16 | if (!cached || !shouldReload(id, cached)) {
17 | return cached
18 | }
19 | debug('Forcing reload: %s', id)
20 | delete rawCache[id]
21 | },
22 | })
23 |
24 | return () => {
25 | // @ts-ignore
26 | Module._cache = rawCache
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/vm/fullReload.ts:
--------------------------------------------------------------------------------
1 | import { Module } from 'module'
2 |
3 | /**
4 | * Create a `shouldReload` function that reloads almost every
5 | * SSR module, avoiding multiple instances of any one module.
6 | */
7 | export function createFullReload(reloadList = new Set()) {
8 | const loadedIds = Object.keys((Module as any)._cache)
9 | const skippedInternals = /\/saus\/(?!client|examples|packages)/
10 |
11 | return (id: string) => {
12 | // Module was possibly cached during the full reload.
13 | if (!loadedIds.includes(id)) {
14 | return false
15 | }
16 | // Modules are reloaded just once per full reload.
17 | if (reloadList.has(id)) {
18 | return false
19 | }
20 | // Internal modules should never be reloaded.
21 | if (skippedInternals.test(id)) {
22 | return false
23 | }
24 | reloadList.add(id)
25 | return true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/vm/index.ts:
--------------------------------------------------------------------------------
1 | export * from './asyncRequire'
2 | export * from './compileEsm'
3 | export * from './debug'
4 | export * from './dedupeNodeResolve'
5 | export * from './executeModule'
6 | export * from './forceNodeReload'
7 | export * from './formatAsyncStack'
8 | export * from './hookNodeResolve'
9 | export * from './ImporterSet'
10 | export * from './moduleMap'
11 | export * from './nodeModules'
12 | export * from './traceNodeRequire'
13 | export * from './types'
14 |
--------------------------------------------------------------------------------
/src/vm/isLiveModule.ts:
--------------------------------------------------------------------------------
1 | import { Merge } from 'type-fest'
2 | import { CompiledModule, isLinkedModule, LinkedModule } from './types'
3 |
4 | /**
5 | * Live modules must have a `module.exports` value that's a plain object
6 | * and its exports must not be destructured by importers.
7 | */
8 | export function isLiveModule(
9 | module: CompiledModule | LinkedModule,
10 | liveModulePaths: Set
11 | ): module is Merge }> {
12 | return (
13 | isLinkedModule(module) &&
14 | module.exports?.constructor == Object &&
15 | liveModulePaths.has(module.id)
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/vm/overwriteScript.ts:
--------------------------------------------------------------------------------
1 | import { combineSourcemaps } from '@utils/combineSourcemaps'
2 | import type { SourceMap } from '@utils/node/sourceMap'
3 | import { Script } from './types'
4 |
5 | export function overwriteScript(
6 | filename: string,
7 | oldScript: Script,
8 | newScript: { code: string; map?: any }
9 | ): Script {
10 | let map: SourceMap | undefined
11 | if (oldScript.map && newScript.map) {
12 | map = combineSourcemaps(filename, [
13 | newScript.map,
14 | oldScript.map as any,
15 | ]) as any
16 | } else {
17 | map = newScript.map || oldScript.map
18 | }
19 | return {
20 | code: newScript.code,
21 | map,
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/vm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saus/vm",
3 | "version": "0.4.10",
4 | "scripts": {
5 | "clean": "rimraf dist && git checkout HEAD dist",
6 | "build": "npm run clean && tsup-node && tsc -p . --emitDeclarationOnly",
7 | "dev": "concurrently npm:dev:*",
8 | "dev:build": "tsup-node --watch --sourcemap",
9 | "dev:types": "tsc -p . --emitDeclarationOnly --watch"
10 | },
11 | "dependencies": {
12 | "@saus/utils": "workspace:*",
13 | "builtin-modules": "^3.2.0",
14 | "debug": "^4.3.2",
15 | "es-module-lexer": "0.9.3",
16 | "kleur": "^4.1.4",
17 | "type-fest": "^2.13.0"
18 | },
19 | "devDependencies": {
20 | "@types/babel__traverse": "^7.18.2",
21 | "@utils": "link:./node_modules/@saus/utils/dist"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/vm/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": ["**/*.ts"],
4 | "exclude": ["**/*.spec.ts", "dist", "tsup.config.ts"],
5 | "compilerOptions": {
6 | "composite": false,
7 | "declaration": true,
8 | "declarationMap": true,
9 | "incremental": true,
10 | "moduleResolution": "node",
11 | "outDir": "dist",
12 | "tsBuildInfoFile": "dist/.tsbuildinfo"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/vm/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions'
2 | import { defineConfig } from 'tsup'
3 |
4 | export default defineConfig({
5 | entry: [
6 | '**/*.ts',
7 | '!**/*.spec.ts',
8 | '!*.config.ts',
9 | '!node_modules/**',
10 | '!dist/**',
11 | ],
12 | outDir: 'dist',
13 | format: ['cjs', 'esm'],
14 | bundle: false,
15 | plugins: [esbuildPluginFilePathExtensions()],
16 | })
17 |
--------------------------------------------------------------------------------
/test/config.ts:
--------------------------------------------------------------------------------
1 | import { vite } from '@/vite'
2 | import { resolve } from 'path'
3 | import { vi } from 'vitest'
4 |
5 | let configFile: {
6 | path: string
7 | config: vite.UserConfig
8 | dependencies: string[]
9 | }
10 |
11 | export const setConfigFile = (root: string, config: vite.UserConfig) =>
12 | (configFile = {
13 | path: resolve(root, 'vite.config.js'),
14 | dependencies: [],
15 | config: {
16 | ...config,
17 | root,
18 | },
19 | })
20 |
21 | vi.mock('@/vite/configFile', (): typeof import('@/vite/configFile') => {
22 | return {
23 | loadConfigFile: async () => configFile,
24 | }
25 | })
26 |
27 | vi.mock('@/vite/configDeps', (): typeof import('@/vite/configDeps') => {
28 | return {
29 | loadConfigDeps: async (_command, { plugins }) => ({
30 | plugins,
31 | }),
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/test/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config'
2 | export * from './context'
3 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | include: ['src/**/*.spec.{ts,tsx}'],
6 | },
7 | server: {
8 | watch: {
9 | ignored: ['**/vendor/**'],
10 | },
11 | },
12 | })
13 |
--------------------------------------------------------------------------------