Skip to content

Commit 2a4fc82

Browse files
committed
feat: Added Plugin Package Configuration + parseAllJsDoc (closes #134)
1 parent 53bc89d commit 2a4fc82

File tree

7 files changed

+281
-159
lines changed

7 files changed

+281
-159
lines changed

projects/core/shared/plugin-types.ts

+16
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,19 @@ export type RawPattern = (
138138
) => ts.Transformer<ts.SourceFile>;
139139

140140
// endregion
141+
142+
/* ****************************************************************************************************************** */
143+
// region: Plugin Package
144+
/* ****************************************************************************************************************** */
145+
146+
export interface PluginPackageConfig {
147+
tscOptions?: {
148+
/**
149+
* Sets the JSDocParsingMode to ParseAll
150+
* @default false
151+
*/
152+
parseAllJsDoc?: boolean;
153+
}
154+
}
155+
156+
// endregion

projects/patch/src/plugin/plugin-creator.ts

+43-34
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@ namespace tsp {
1414
ls?: tsShim.LanguageService;
1515
}
1616

17+
export namespace PluginCreator {
18+
export interface Options {
19+
resolveBaseDir: string;
20+
}
21+
}
22+
1723
// endregion
1824

1925
/* ********************************************************* */
2026
// region: Helpers
2127
/* ********************************************************* */
2228

23-
function validateConfigs(configs: PluginConfig[]) {
24-
for (const config of configs)
25-
if (!config.name && !config.transform) throw new TsPatchError('tsconfig.json plugins error: transform must be present');
26-
}
27-
2829
function createTransformerFromPattern(opt: CreateTransformerFromPatternOptions): TransformerBasePlugin {
2930
const { factory, config, program, ls, registerConfig } = opt;
3031
const { transform, after, afterDeclarations, name, type, transformProgram, ...cleanConfig } = config;
@@ -131,49 +132,55 @@ namespace tsp {
131132
* PluginCreator (Class)
132133
* ********************************************************* */
133134

134-
/**
135-
* @example
136-
*
137-
* new PluginCreator([
138-
* {transform: '@zerollup/ts-transform-paths', someOption: '123'},
139-
* {transform: '@zerollup/ts-transform-paths', type: 'ls', someOption: '123'},
140-
* {transform: '@zerollup/ts-transform-paths', type: 'ls', after: true, someOption: '123'}
141-
* ]).createTransformers({ program })
142-
*/
143135
export class PluginCreator {
144-
constructor(
145-
private configs: PluginConfig[],
146-
public resolveBaseDir: string = process.cwd()
147-
)
148-
{
149-
validateConfigs(configs);
136+
public readonly plugins: TspPlugin[] = [];
137+
public readonly options: PluginCreator.Options;
138+
public readonly needsTscJsDocParsing: boolean;
139+
140+
private readonly configs: PluginConfig[];
141+
142+
constructor(configs: PluginConfig[], options: PluginCreator.Options) {
143+
this.configs = configs;
144+
this.options = options;
145+
146+
const { resolveBaseDir } = options;
147+
148+
/* Create plugins */
149+
this.plugins = configs.map(config => new TspPlugin(config, { resolveBaseDir }));
150+
151+
/* Check if we need to parse all JSDoc comments */
152+
this.needsTscJsDocParsing = this.plugins.some(plugin => plugin.packageConfig?.tscOptions?.parseAllJsDoc === true);
150153
}
151154

152-
public mergeTransformers(into: TransformerList, source: tsShim.CustomTransformers | TransformerBasePlugin) {
155+
private mergeTransformers(into: TransformerList, source: tsShim.CustomTransformers | TransformerBasePlugin) {
153156
const slice = <T>(input: T | T[]) => (Array.isArray(input) ? input.slice() : [ input ]);
154157

158+
// TODO : Consider making this optional https://github.com/nonara/ts-patch/issues/122
159+
155160
if (source.before) into.before.push(...slice(source.before));
156161
if (source.after) into.after.push(...slice(source.after));
157162
if (source.afterDeclarations) into.afterDeclarations.push(...slice(source.afterDeclarations));
158163

159164
return this;
160165
}
161166

162-
public createTransformers(
167+
public createSourceTransformers(
163168
params: { program: tsShim.Program } | { ls: tsShim.LanguageService },
164169
customTransformers?: tsShim.CustomTransformers
165170
): TransformerList {
166171
const transformers: TransformerList = { before: [], after: [], afterDeclarations: [] };
167172

168173
const [ ls, program ] = ('ls' in params) ? [ params.ls, params.ls.getProgram()! ] : [ void 0, params.program ];
169174

170-
for (const config of this.configs) {
171-
if (!config.transform || config.transformProgram) continue;
175+
for (const plugin of this.plugins) {
176+
if (plugin.kind !== 'SourceTransformer') continue;
177+
178+
const { config } = plugin;
172179

173-
const resolvedFactory = tsp.resolveFactory(this, config);
174-
if (!resolvedFactory) continue;
180+
const createFactoryResult = plugin.createFactory();
181+
if (!createFactoryResult) continue;
175182

176-
const { factory, registerConfig } = resolvedFactory;
183+
const { factory, registerConfig } = createFactoryResult;
177184

178185
this.mergeTransformers(
179186
transformers,
@@ -193,16 +200,18 @@ namespace tsp {
193200
return transformers;
194201
}
195202

196-
public getProgramTransformers(): Map<string, [ ProgramTransformer, PluginConfig ]> {
203+
public createProgramTransformers(): Map<string, [ ProgramTransformer, PluginConfig ]> {
197204
const res = new Map<string, [ ProgramTransformer, PluginConfig ]>();
198-
for (const config of this.configs) {
199-
if (!config.transform || !config.transformProgram) continue;
205+
for (const plugin of this.plugins) {
206+
if (plugin.kind !== 'ProgramTransformer') continue;
207+
208+
const { config } = plugin;
200209

201-
const resolvedFactory = resolveFactory(this, config);
202-
if (resolvedFactory === undefined) continue;
210+
const createFactoryResult = plugin.createFactory();
211+
if (createFactoryResult === undefined) continue;
203212

204-
const { registerConfig } = resolvedFactory;
205-
const factory = wrapTransformer(resolvedFactory.factory as ProgramTransformer, registerConfig, false);
213+
const { registerConfig, factory: unwrappedFactory } = createFactoryResult;
214+
const factory = wrapTransformer(unwrappedFactory as ProgramTransformer, registerConfig, false);
206215

207216
const transformerKey = crypto
208217
.createHash('md5')

projects/patch/src/plugin/plugin.ts

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
namespace tsp {
2+
const path = require('path');
3+
const fs = require('fs');
4+
5+
const requireStack: string[] = [];
6+
7+
/* ****************************************************** */
8+
// region: Types
9+
/* ****************************************************** */
10+
11+
export namespace TspPlugin {
12+
export interface CreateOptions {
13+
resolveBaseDir: string
14+
}
15+
16+
export type Kind = 'SourceTransformer' | 'ProgramTransformer'
17+
}
18+
19+
// endregion
20+
21+
/* ****************************************************** */
22+
// region: Helpers
23+
/* ****************************************************** */
24+
25+
function getModulePackagePath(transformerPath: string, resolveBaseDir: string): string | undefined {
26+
let transformerPackagePath: string | undefined;
27+
try {
28+
const pathQuery = path.join(transformerPath, 'package.json');
29+
transformerPackagePath = path.normalize(require.resolve(pathQuery, { paths: [ resolveBaseDir ] }));
30+
} catch (e) {
31+
return undefined;
32+
}
33+
34+
let currentDir = path.dirname(transformerPath);
35+
36+
const seenPaths = new Set<string>();
37+
while (currentDir !== path.parse(currentDir).root) {
38+
if (seenPaths.has(currentDir)) return undefined;
39+
seenPaths.add(currentDir);
40+
41+
// Could likely fail if the transformer is in a symlinked directory or the package's main file is in a
42+
// directory above the package.json – however, I believe that the walking up method used here is the common
43+
// approach, so we'll consider these acceptable edge cases for now.
44+
if (path.relative(currentDir, transformerPackagePath).startsWith('..')) return undefined;
45+
46+
const potentialPkgPath = path.join(currentDir, 'package.json');
47+
if (fs.existsSync(potentialPkgPath)) {
48+
if (potentialPkgPath === transformerPackagePath) return transformerPackagePath;
49+
return undefined;
50+
}
51+
52+
currentDir = path.resolve(currentDir, '..');
53+
}
54+
55+
return undefined;
56+
}
57+
58+
// endregion
59+
60+
/* ****************************************************** */
61+
// region: TspPlugin
62+
/* ****************************************************** */
63+
64+
export class TspPlugin {
65+
public readonly config: PluginConfig;
66+
public readonly tsConfigPath: string | undefined;
67+
public readonly entryFilePath: string;
68+
public readonly importKey: string;
69+
public readonly packageConfig: PluginPackageConfig | undefined;
70+
public readonly kind: TspPlugin.Kind;
71+
72+
private readonly _createOptions: TspPlugin.CreateOptions;
73+
74+
constructor(config: PluginConfig, createOptions: TspPlugin.CreateOptions) {
75+
this.config = { ...config };
76+
this.validateConfig();
77+
78+
this._createOptions = createOptions;
79+
this.importKey = config.import || 'default';
80+
this.kind = config.transformProgram === true ? 'ProgramTransformer' : 'SourceTransformer';
81+
82+
const { resolveBaseDir } = createOptions;
83+
const configTransformValue = config.transform!;
84+
85+
/* Resolve paths */
86+
this.tsConfigPath = config.tsConfig && path.resolve(resolveBaseDir, config.tsConfig);
87+
const entryFilePath = require.resolve(configTransformValue, { paths: [ resolveBaseDir ] });
88+
this.entryFilePath = entryFilePath;
89+
90+
/* Get module PluginPackageConfig */
91+
const modulePackagePath = getModulePackagePath(entryFilePath, resolveBaseDir);
92+
let pluginPackageConfig: PluginPackageConfig | undefined;
93+
if (modulePackagePath) {
94+
const modulePkgJsonContent = fs.readFileSync(modulePackagePath, 'utf8');
95+
const modulePkgJson = JSON.parse(modulePkgJsonContent) as { tsp?: PluginPackageConfig };
96+
97+
pluginPackageConfig = modulePkgJson.tsp;
98+
if (pluginPackageConfig === null || typeof pluginPackageConfig !== 'object') pluginPackageConfig = undefined;
99+
}
100+
101+
this.packageConfig = pluginPackageConfig;
102+
}
103+
104+
private validateConfig() {
105+
const { config } = this;
106+
107+
const configTransformValue = config.transform;
108+
if (!configTransformValue) throw new TsPatchError(`Invalid plugin config: missing "transform" value`);
109+
110+
if (config.resolvePathAliases && !config.tsConfig) {
111+
console.warn(`[ts-patch] Warning: resolvePathAliases needs a tsConfig value pointing to a tsconfig.json for transformer" ${configTransformValue}.`);
112+
}
113+
}
114+
115+
createFactory() {
116+
const { entryFilePath, config, tsConfigPath, importKey } = this;
117+
const configTransformValue = config.transform!;
118+
119+
/* Prevent circular require */
120+
if (requireStack.includes(entryFilePath)) return;
121+
requireStack.push(entryFilePath);
122+
123+
/* Check if ESM */
124+
let isEsm: boolean | undefined = config.isEsm;
125+
if (isEsm == null) {
126+
const impliedModuleFormat = tsShim.getImpliedNodeFormatForFile(
127+
entryFilePath as tsShim.Path,
128+
undefined,
129+
tsShim.sys,
130+
{ moduleResolution: tsShim.ModuleResolutionKind.Node16 }
131+
);
132+
133+
isEsm = impliedModuleFormat === tsShim.ModuleKind.ESNext;
134+
}
135+
136+
const isTs = configTransformValue.match(/\.[mc]?ts$/) != null;
137+
138+
const registerConfig: RegisterConfig = {
139+
isTs,
140+
isEsm,
141+
tsConfig: tsConfigPath,
142+
pluginConfig: config
143+
};
144+
145+
registerPlugin(registerConfig);
146+
147+
try {
148+
/* Load plugin */
149+
const commonjsModule = loadEntryFile();
150+
151+
const factoryModule = (typeof commonjsModule === 'function') ? { default: commonjsModule } : commonjsModule;
152+
const factory = factoryModule[importKey];
153+
154+
if (!factory)
155+
throw new TsPatchError(
156+
`tsconfig.json > plugins: "${configTransformValue}" does not have an export "${importKey}": ` +
157+
require('util').inspect(factoryModule)
158+
);
159+
160+
if (typeof factory !== 'function') {
161+
throw new TsPatchError(
162+
`tsconfig.json > plugins: "${configTransformValue}" export "${importKey}" is not a plugin: ` +
163+
require('util').inspect(factory)
164+
);
165+
}
166+
167+
return {
168+
factory,
169+
registerConfig: registerConfig
170+
};
171+
}
172+
finally {
173+
requireStack.pop();
174+
unregisterPlugin();
175+
}
176+
177+
function loadEntryFile(): PluginFactory | { [key: string]: PluginFactory } {
178+
/* Load plugin */
179+
let res: PluginFactory | { [key: string]: PluginFactory }
180+
try {
181+
res = require(entryFilePath);
182+
} catch (e) {
183+
if (e.code === 'ERR_REQUIRE_ESM') {
184+
if (!registerConfig.isEsm) {
185+
unregisterPlugin();
186+
registerConfig.isEsm = true;
187+
registerPlugin(registerConfig);
188+
return loadEntryFile();
189+
} else {
190+
throw new TsPatchError(
191+
`Cannot load ESM transformer "${configTransformValue}" from "${entryFilePath}". Please file a bug report`
192+
);
193+
}
194+
}
195+
else throw e;
196+
}
197+
return res;
198+
}
199+
}
200+
}
201+
202+
// endregion
203+
}
204+
205+
// endregion

0 commit comments

Comments
 (0)