-
Notifications
You must be signed in to change notification settings - Fork 20
/
san-project.ts
164 lines (147 loc) · 7.22 KB
/
san-project.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
* San 项目
*
* 一个 SanProject 实例表示一个 San 项目,同一个项目中多次编译可以复用一些已经建立好的对象。
* 尤其对于从 TypeScript 源码开始 SSR 的情况,多次编译之间,整个 TypeScript 项目里的所有(包括类型)文件是可以缓存的。
*
* 对使用者建议如下:
*
* 1. 简单的 SSR 可以直接用 src/index.ts 里的 compileToRenderer 和 compileToSource。
* 2. 复杂的编译过程建议使用 SanProject,它提供了更精细的 API。上述两个 API 其实是 SanProject 一个包装。
*/
import type { ComponentConstructor } from 'san'
import type { TypedSanSourceFile, DynamicSanSourceFile, SanSourceFile, JSSanSourceFile } from '../models/san-source-file'
import type { parseSanSourceFileOptions, RenderOptions } from '../compilers/renderer-options'
import type { Renderer } from './renderer'
import type { CompileOptions } from '../target-js/compilers/compile-options'
import type { TargetCodeGenerator } from '../models/target-code-generator'
import assert from 'assert'
import { Project } from 'ts-morph'
import { ComponentClassParser } from '../parsers/component-class-parser'
import { TypeScriptSanParser } from '../parsers/typescript-san-parser'
import { JavaScriptSanParser } from '../parsers/javascript-san-parser'
import { SanFileParser } from '../parsers/san-file-parser'
import { removeModules } from '../parsers/remove-modules'
import ToJSCompiler from '../target-js/index'
import { getDefaultTSConfigPath } from '../parsers/tsconfig'
import { isFileDescriptor, isSanFileDescriptor, isComponentClass, ComponentClass, FileDescriptor, CompileInput } from './options'
type TargetCodeGeneratorClass<T extends TargetCodeGenerator = TargetCodeGenerator> = { new(project: SanProject): T }
/**
* SanProject 对应于 TypeScript 项目,即 tsconfig.json 及其引用的所有文件构成的集合。
*/
export class SanProject {
public tsProject?: Project
private compilers: Map<TargetCodeGeneratorClass, TargetCodeGenerator> = new Map()
constructor (public tsConfigFilePath: null | string | undefined = getDefaultTSConfigPath()) {
if (tsConfigFilePath !== null) {
this.tsProject = new Project({ tsConfigFilePath, addFilesFromTsConfig: false })
}
}
/**
* 兼容旧版用法
* @alias SanProject.compileToSource
*/
public compile (input: CompileInput, target: string | TargetCodeGeneratorClass = 'js', options = {}) {
return this.compileToSource(input, target, options)
}
/**
* 源文件/组件类编译到源代码
*/
public compileToSource<T extends TargetCodeGenerator> (
input: CompileInput,
target: string | TargetCodeGeneratorClass<T> = 'js',
options: RenderOptions = {}
) {
const sanSourceFile = this.parseSanSourceFile(input, { sanReferenceInfo: options.sanReferenceInfo })
// 删除配置中指定的在 ssr 下无需引入的模块
const { removeModules: modules } = options
if (modules && modules.length) {
removeModules(sanSourceFile, modules)
}
const compiler = this.getOrCreateTargetCodeGenerator(target)
return compiler.compileToSource(sanSourceFile, options)
}
/**
* 解析 CompileInput 实例中的 San 相关信息
*
* CompileInput 可以是 JS、TS 源文件,也可以是组件类,得到的 SanSourceFile 里包含这个文件里
* San 相关的信息,比如有多少个组件?每个组件有哪些方法?以及得到 template 对应的 ANode 树。
*/
public parseSanSourceFile (componentClass: ComponentClass, options?: parseSanSourceFileOptions): DynamicSanSourceFile
public parseSanSourceFile (fileDescriptor: FileDescriptor, options?: parseSanSourceFileOptions): TypedSanSourceFile
public parseSanSourceFile (filecontent: string, options?: parseSanSourceFileOptions): JSSanSourceFile
public parseSanSourceFile (input: CompileInput, options?: parseSanSourceFileOptions): SanSourceFile
public parseSanSourceFile (input: CompileInput, options?: parseSanSourceFileOptions): SanSourceFile {
if (isComponentClass(input)) return new ComponentClassParser(input, '').parse()
if (isSanFileDescriptor(input)) {
return new SanFileParser(input.scriptContent, input.templateContent, input.filePath).parse()
}
const filePath = isFileDescriptor(input) ? input.filePath : input
const fileContent = isFileDescriptor(input) ? input.fileContent : undefined
if (/\.ts$/.test(filePath)) {
if (!this.tsProject) throw new Error(`Error parsing ${input}, tsconfig not specified`)
const sourceFile = fileContent
? this.tsProject.createSourceFile(filePath, fileContent, { overwrite: true })
: this.tsProject.addSourceFileAtPath(filePath)
!fileContent && sourceFile.refreshFromFileSystemSync()
return new TypeScriptSanParser().parse(sourceFile)
}
return new JavaScriptSanParser(filePath, undefined, 'script', options).parse()
}
/**
* 编译成当前 JavaScript 进程里的 render 函数
*
* * `target` 固定为 "js"
* * `options.bareFunction` 固定为 true
*/
public compileToRenderer (
componentClass: ComponentConstructor<{}, any>,
options?: CompileOptions
): Renderer {
const sanSourceFile = new ComponentClassParser(componentClass, '').parse()
const compiler = this.getOrCreateTargetCodeGenerator(ToJSCompiler)
return compiler.compileToRenderer(sanSourceFile, options)
}
/**
* 输出工具库:组件渲染时需要使用的公共工具。
*/
public emitHelpers (target: string, options: any = {}) {
const compiler = this.getOrCreateTargetCodeGenerator(target)
assert(compiler.emitHelpers, `emit helpers not supported by "${target}"`)
return compiler.emitHelpers(options)
}
public getCompilerOptionsOrThrow () {
return this.tsProject!.getCompilerOptions()
}
/**
* 得到目标代码生成器的实例
*
* 如果不存在就去加载一个,如果已经加载过就把它存起来。
*/
public getOrCreateTargetCodeGenerator<T extends TargetCodeGenerator = TargetCodeGenerator> (target: string | TargetCodeGeneratorClass<T>): T {
const CC: TargetCodeGeneratorClass<T> = this.loadTargetCodeGenerator(target)
if (!this.compilers.has(CC)) {
this.compilers.set(CC, new CC(this))
}
return this.compilers.get(CC) as T
}
/**
* 加载目标代码生成器
*
* 如果是 san-ssr-target-js,直接从当前仓库的子目录下加载。
* 如果是其他的 target,用 require.resolve 来找。
*/
private loadTargetCodeGenerator (target: string | TargetCodeGeneratorClass) {
if (typeof target !== 'string') return target
const name = `san-ssr-target-${target}`
if (name === 'san-ssr-target-js') return ToJSCompiler
let path
try {
path = require.resolve(name)
} catch (e) {
throw new Error(`failed to load "san-ssr-target-${target}"`)
}
const plugin = require(path)
return plugin.default || plugin
}
}