Skip to content

Commit

Permalink
feat!: load and transpile modules on demand
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed Dec 7, 2023
1 parent 7990398 commit 8d2d130
Show file tree
Hide file tree
Showing 43 changed files with 1,172 additions and 272 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@
"node-fetch": "^3.2.10",
"puppeteer-core": "^21.5.1",
"resolve": "^1.22.1",
"rollup": "^2.78.1",
"rollup": "^4.6.0",
"rollup-plugin-external-globals": "^0.8.0",
"rollup-plugin-node-builtins": "^2.1.2",
"source-map": "^0.7.4"
},
"devDependencies": {
"@ph.fritsche/eslint-config": "^3.0.0-beta",
"@ph.fritsche/scripts-config": "^2.4.0",
"@sinonjs/fake-timers": "^11.2.2",
"@types/istanbul-lib-instrument": "^1.7.4",
"@types/istanbul-lib-report": "^3.0.0",
"@types/istanbul-lib-source-maps": "^4.0.1",
"@types/istanbul-reports": "^3.0.1",
"@types/node": "^18",
"@types/resolve": "^1.20.2",
"@types/sinonjs__fake-timers": "^8.1.5",
"eslint": "^8.39.0",
"expect": "^29.5.0",
"globals": "^13.20.0",
Expand Down
4 changes: 2 additions & 2 deletions src/builder/Builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class Builder {
entryFileNames: (outputOptions?.preserveModules ?? true)
? undefined
: preserveEntryFileNames(basePath),
sourcemap: true,
sourcemap: 'inline',
paths: (id: string) => {
if (id.startsWith('.')) {
return id
Expand Down Expand Up @@ -302,7 +302,7 @@ function setOutputFiles(
map.set(f.fileName, {
moduleId: f.facadeModuleId,
isEntry: f.isEntry,
content: `${f.code}\n//# sourceMappingURL=${String(f.map?.toUrl())}`,
content: f.code,
})
} else {
map.set(f.fileName, {
Expand Down
34 changes: 24 additions & 10 deletions src/builder/index.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,58 @@
import fsSync from 'node:fs'
import os from 'node:os'
import { OutputOptions } from 'rollup'
import createCjsPlugin from '@rollup/plugin-commonjs'
import createNodeBuiltinsPlugin from 'rollup-plugin-node-builtins'
import { parseTsConfig } from './tsconfig'
import { createTsResolvePlugin, createNodeResolvePlugin, createNodeCoreResolvePlugin } from './plugins/resolve'
import { createTransformPlugin } from './plugins/transform'
import { createIstanbulPlugin } from './plugins/instrument'
import { createNodeCoreEntryFileNames, createNodeCorePaths, createNodePolyfillPlugin, createNodeReexportPlugin } from './plugins/node'
import { Builder } from './Builder'
import { isBuiltin } from 'node:module'
import { createUndefinedPlugin } from './plugins/undefined'
import { createCachePlugin, CachePluginOptions } from './plugins/cache'
import { createJsonPlugin } from './plugins/json'
import { createGlobalsPlugin } from './plugins/globals'
import { CachedFilesystem, SyncFilesystem } from '../files'
import { TsConfigResolver, TsModuleResolver } from '../ts'

export { Builder } from './Builder'
export type { OutputFilesMap } from './Builder'
export { BuildProvider } from './BuildProvider'

export function createSourceBuilder(
{
tsConfigFile,
coverageVariable = '__coverage__',
instrument = s => !/(^|\/)node_modules\//.test(s),
globals,
fs = new CachedFilesystem({
caseSensitive: os.platform() !== 'win32',
existsSync: fsSync.existsSync,
readFileSync: fsSync.readFileSync,
realpathSync: fsSync.realpathSync,
}),
tsConfigResolver = new TsConfigResolver(fs),
tsModuleResolver = new TsModuleResolver(fs),
}: {
tsConfigFile: string
globals?: OutputOptions['globals']
coverageVariable?: string
instrument?: (subPath: string) => boolean,
globals?: OutputOptions['globals'],
fs?: SyncFilesystem,
tsConfigResolver?: TsConfigResolver,
tsModuleResolver?: TsModuleResolver,
},
id = 'project',
) {
const { compilerOptions } = parseTsConfig(tsConfigFile)

return new Builder({
id,
plugins: [
createTsResolvePlugin(compilerOptions),
createTsResolvePlugin(tsConfigResolver, tsModuleResolver),
createNodeResolvePlugin(),
createNodeCoreResolvePlugin(),
createJsonPlugin(),
createTransformPlugin(compilerOptions),
createTransformPlugin({
coverageVariable: s => instrument(s) ? coverageVariable : undefined,
}),
createGlobalsPlugin({globals}),
createIstanbulPlugin(),
],
paths: createNodeCorePaths(),
outputOptions: {
Expand Down
47 changes: 7 additions & 40 deletions src/builder/plugins/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { stat } from 'fs/promises'
import path from 'path'
import { Plugin } from 'rollup'
import ts, { CompilerOptions } from 'typescript'
import requireResolveAsync from 'resolve'
import { isBuiltin } from 'node:module'
import type { TsConfigResolver, TsModuleResolver } from '../../ts'

function requireResolve(moduleName: string, importer: string) {
return new Promise<string|undefined>((res, rej) => {
Expand All @@ -19,39 +19,6 @@ function requireResolve(moduleName: string, importer: string) {
})
}

const _resolvedTsPath: Record<string, string | undefined | false> = {}
async function resolveTsPaths(
moduleName: string,
{
baseUrl = '.',
paths,
}: ts.CompilerOptions,
) {
if (moduleName in _resolvedTsPath) {
return _resolvedTsPath[moduleName]
}
let fail: undefined | false
for (const [key, mapping] of Object.entries(paths ?? [])) {
const re = new RegExp(`^${key.replace('*', '(.*)')}$`)
const m = moduleName.match(re)
if (m) {
fail = false
for (let p of mapping) {
m.slice(1).forEach(p_ => {
p = p.replace('*', p_)
})
const f = await matchFiles(p, baseUrl)
if (f) {
_resolvedTsPath[moduleName] = f
return f
}
}
}
}
_resolvedTsPath[moduleName] = fail
return fail
}

const _absPath: Record<string, string> = {}
const _matchedFiles: Record<string, string | undefined> = {}
async function matchFiles(
Expand Down Expand Up @@ -90,20 +57,20 @@ async function matchFiles(
}

export function createTsResolvePlugin(
compilerOptions: CompilerOptions,
configResolver: TsConfigResolver,
moduleResolver: TsModuleResolver,
name = 'ts-import-resolver',
): Plugin {
return {
name,
async resolveId(moduleName, importer) {
resolveId(moduleName, importer) {
if (!importer || !/\.tsx?$/.test(importer)) {
return undefined
}
const resolved = moduleName.startsWith('.')
? await matchFiles(moduleName, path.dirname(importer))
: await resolveTsPaths(moduleName, compilerOptions)

return resolved
const compilerOptions = configResolver.getCompilerOptions(path.dirname(importer))

return moduleResolver.resolveModule(moduleName, importer, compilerOptions)
},
}
}
Expand Down
75 changes: 52 additions & 23 deletions src/builder/plugins/transform.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { CompilerOptions } from 'typescript'
import { Plugin } from 'rollup'
import { transform as swcTransform, Options as swcOptions } from '@swc/core'
import { JscTarget, ParseOptions, transform } from '@swc/core'

export function createTransformPlugin(
tsCompilerOptions: CompilerOptions,
{
cwd = process.cwd(),
coverageVariable: getCoverageVar,
}: {
cwd?: string,
coverageVariable?: (subPath: string) => string|undefined,
},
name = 'script-transformer',
): Plugin {
return {
name,
async transform(code, id) {
async transform(source, id) {
if (!/\.[jt]sx?(\?.*)?$/.test(id)
// || id.includes('/node_modules/')
) {
Expand All @@ -18,30 +23,54 @@ export function createTransformPlugin(
return ''
}

const options: swcOptions = {
sourceFileName: id,
const [path] = id.split(/\?#/, 1)
const subPath = path.startsWith(cwd + '/') ? path.substring(cwd.length + 1) : undefined

const coverageVariable = subPath && getCoverageVar?.(subPath)

const target: JscTarget = 'es2022'

const isTs = /\.tsx?/.test(path)
const parseOptions: ParseOptions = isTs
? {
syntax: 'typescript',
tsx: path.endsWith('x'),
target,
}
: {
syntax: 'ecmascript',
jsx: path.endsWith('x'),
target,
}

const { code, map } = await transform(source, {
filename: path,
sourceFileName: path,
module: {
type: 'es6',
ignoreDynamic: true,
},
jsc: {
externalHelpers: false,
parser: {
syntax: /\.tsx?$/.test(id) ? 'typescript' : 'ecmascript',
jsx: id.endsWith('x'),
tsx: id.endsWith('x'),
},
target: 'es2019',
transform: {
react: {
pragma: tsCompilerOptions.jsxFactory,
pragmaFrag: tsCompilerOptions.jsxFragmentFactory,
},
target: 'es2022',
parser: parseOptions,
preserveAllComments: true,
experimental: {
plugins: coverageVariable
? [
['swc-plugin-coverage-instrument', {
coverageVariable,
}],
]
: [],
},
// Rollup produces invalid import paths that can't be resolved with this.
// baseUrl: tsBaseUrl + '/',
// paths: tsCompilerOptions.paths as Record<string, [string]>,
},
sourceMaps: true,
}
})

return swcTransform(code, options)
return {
code,
map,
}
},
}
}
5 changes: 4 additions & 1 deletion src/conductor/TestRun/TestRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { TestRunInstanceIndex, TestRunStackIndex } from './TestIndex'
import { TestNodeInstance, TestNodeStack } from './TestNode'
import { TestSuite, TestSuiteStack } from './TestSuite'

type TestFile = {url: string, title: string}
export type TestFile = {
url: string
title: string
}

export class TestRunStack extends TestNodeStack<TestRunInstance> {
static create(
Expand Down
2 changes: 1 addition & 1 deletion src/conductor/TestRun/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type { TestNodeStack, TestNodeInstance } from './TestNode'
export type { TestRunStack, TestRunInstance} from './TestRun'
export type { TestRunStack, TestRunInstance, TestFile } from './TestRun'
export type { TestSuite, TestSuiteStack } from './TestSuite'
export type { TestGroup } from './TestGroup'
export type { TestHook } from './TestHook'
Expand Down
26 changes: 11 additions & 15 deletions src/conductor/TestRunIterator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { TestNodeInstance, TestNodeStack, TestRunStack } from './TestRun'

export class TestRunIterator {
static *ancestors<T extends TestNodeInstance|TestNodeStack>(
export const TestRunIterator = {
ancestors: function* <T extends TestNodeInstance|TestNodeStack>(
node: T,
) {
for (let el: T|undefined = node; el; el = el.parent as T|undefined) {
yield el
}
}

static *stackTree(
},
stackTree: function* (
nodes: Iterable<TestNodeStack>,
): Generator<TestNodeStack> {
for (const n of nodes) {
Expand All @@ -18,9 +17,8 @@ export class TestRunIterator {
yield* this.stackTree(n.children.values())
}
}
}

static *instanceTree(
},
instanceTree: function* (
nodes: Iterable<TestNodeInstance>,
): Generator<TestNodeInstance> {
for (const n of nodes) {
Expand All @@ -29,25 +27,23 @@ export class TestRunIterator {
yield* this.instanceTree(n.children.values())
}
}
}

static *iterateSuitesByConductors(
},
iterateSuitesByConductors: function* (
runStack: TestRunStack,
) {
for (const r of runStack.runs.values()) {
for (const s of r.suites.values()) {
yield s
}
}
}

static *iterateConductorsBySuites(
},
iterateConductorsBySuites: function* (
runStack: TestRunStack,
) {
for (const s of runStack.suites.values()) {
for (const r of s.instances.values()) {
yield r
}
}
}
},
}
6 changes: 3 additions & 3 deletions src/ts/CachedFilesystem.ts → src/files/CachedFilesystem.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Filesystem } from './Filesystem'
import { SyncFilesystem } from './Filesystem'

class CachedError {
constructor(
public error: unknown,
) {}
}

export class CachedFilesystem implements Filesystem {
export class CachedFilesystem implements SyncFilesystem {
constructor(
protected readonly filesystem: Filesystem,
protected readonly filesystem: SyncFilesystem,
) {}
protected resolveCache: Record<string, string> = {}
protected existsCache: Record<string, boolean> = {}
Expand Down

0 comments on commit 8d2d130

Please sign in to comment.