-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathConstructionStrategyCommonJsString.ts
More file actions
200 lines (179 loc) · 7.26 KB
/
ConstructionStrategyCommonJsString.ts
File metadata and controls
200 lines (179 loc) · 7.26 KB
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import * as Path from 'path';
import type { IModuleState } from '../../loading/ModuleStateBuilder';
import type { ICreationStrategyCommonJsOptions } from './ConstructionStrategyCommonJs';
import { ConstructionStrategyCommonJs } from './ConstructionStrategyCommonJs';
import type {
IConstructionStrategy,
ICreationStrategyHashOptions,
ICreationStrategyInstanceOptions,
ICreationStrategyArrayOptions,
ICreationStrategyPrimitiveOptions,
ICreationStrategySupplierOptions,
ICreationStrategyVariableOptions,
} from './IConstructionStrategy';
/**
* A creation strategy for a string representation of CommonJS.
*
* When this strategy is plugged into a {@link ComponentsManager},
* the manager will output a string that represents the name of the variable that has been instantiated.
* In order to retrieve a string representation of all Common JS logic to construct this variable,
* the {@link serializeDocument} method can be invoked with this variable string.
*
* A typical pattern for using this strategy looks as follows:
* ```
const serializationVariableName = await manager.instantiate(configIri);
const document = constructionStrategy.serializeDocument(serializationVariableName);
* ```
*
* @see compileConfig For a simplified abstraction for using this strategy.
*/
export class ConstructionStrategyCommonJsString implements IConstructionStrategy<string> {
private readonly overrideRequireNames: Record<string, string>;
private readonly asFunction: boolean;
private readonly strategyCommonJs: ConstructionStrategyCommonJs;
private readonly lines: string[] = [];
// eslint-disable-next-line unicorn/no-object-as-default-parameter
public constructor(options: ICreationStrategyCommonJsStringOptions = { req: require }) {
this.overrideRequireNames = options.overrideRequireNames || {};
this.asFunction = Boolean(options.asFunction);
this.strategyCommonJs = new ConstructionStrategyCommonJs(options);
}
public createInstance(options: ICreationStrategyInstanceOptions<string>): string {
// Call require()
options.requireName = this.overrideRequireNames[options.requireName] || options.requireName;
// First try requiring current module, and fallback to a plain require
const currentResult = this.strategyCommonJs
.requireCurrentRunningModuleIfCurrent(options.moduleState, options.requireName);
const resultingRequirePath = currentResult !== false ?
`.${Path.sep}${Path.relative(
options.moduleState.mainModulePath,
this.getCurrentRunningModuleMain(options.moduleState),
)}` :
options.requireName;
let serialization = `require('${resultingRequirePath.replaceAll('\\', '/')}')`;
// Determine the child of the require'd element
if (options.requireElement) {
serialization += `.${options.requireElement}`;
}
// Call the constructor of the element
if (options.callConstructor) {
serialization = `new (${serialization})(${options.args.join(', ')})`;
}
// Add a line to our file to declare the instantiated element as a const
const serializationVariableName = ConstructionStrategyCommonJsString.uriToVariableName(options.instanceId);
serialization = `const ${serializationVariableName} = ${serialization};`;
this.lines.push(serialization);
serialization = serializationVariableName;
return serialization;
}
/**
* Get the path to the main module's main entrypoint.
* @param moduleState The module state.
* @return {string} The index module path of the current running module (`"main"` entry in package.json).
*/
public getCurrentRunningModuleMain(moduleState: IModuleState): string {
const pckg = moduleState.packageJsons[moduleState.mainModulePath];
return Path.join(moduleState.mainModulePath, pckg.main);
}
public createHash(options: ICreationStrategyHashOptions<string>): string {
const sb: string[] = [ '{' ];
for (const entry of options.entries) {
if (entry) {
if (sb.length > 1) {
sb.push(',');
}
sb.push('\n');
sb.push(' ');
sb.push(`${entry.key}`);
sb.push(': ');
sb.push(entry.value);
}
}
if (sb.length > 1) {
sb.push('\n');
}
sb.push('}');
return sb.join('');
}
public createArray(options: ICreationStrategyArrayOptions<string>): string {
const sb: string[] = [ '[' ];
for (const value of options.elements) {
if (sb.length > 1) {
sb.push(',');
}
sb.push('\n');
sb.push(' ');
sb.push(value);
}
if (sb.length > 1) {
sb.push('\n');
}
sb.push(']');
return sb.join('');
}
public async createLazySupplier(options: ICreationStrategySupplierOptions<string>): Promise<string> {
return `new function() { return Promise.resolve(${await options.supplier()}); }`;
}
public createPrimitive(options: ICreationStrategyPrimitiveOptions<string>): string {
if (typeof options.value === 'object') {
return JSON.stringify(options.value);
}
return typeof options.value === 'string' ? `'${options.value}'` : `${options.value}`;
}
public getVariableValue(options: ICreationStrategyVariableOptions<string>): string {
if (this.asFunction) {
return `getVariableValue('${options.variableName}')`;
}
throw new Error(`Detected a variable during config compilation: ${options.variableName}. Variables are not supported, but require the -f flag to expose the compiled config as function.`);
}
public createUndefined(): string {
return 'undefined';
}
/**
* Deterministically converts a URI to a variable name that is safe for usage within JavaScript.
* @param {string} uri A URI.
* @return {string} A variable name.
*/
public static uriToVariableName(uri: string): string {
return uri.replace(/[#./:@\\^-]/gu, '_');
}
/**
* Serialize a full Common JS document to a string.
* @param serializationVariableName The resulting string when calling {@link ComponentsManager.instantiate}.
* @param exportVariableName An optional variable name that should be exported
* instead of the default (serializationVariableName).
*/
public serializeDocument(serializationVariableName: string, exportVariableName?: string): string {
// Join all lines in the document
const document: string = this.lines.join('\n');
// Override main variable name if needed
exportVariableName = (exportVariableName ?
ConstructionStrategyCommonJsString.uriToVariableName(exportVariableName) :
exportVariableName) || serializationVariableName;
// Export as variable-based function
if (this.asFunction) {
return `module.exports = function(variables) {
function getVariableValue(name) {
if (!variables || !(name in variables)) {
throw new Error('Undefined variable: ' + name);
}
return variables[name];
}
${document}
return ${exportVariableName};
}
`;
}
// Direct export of instantiated component
return `${document}
module.exports = ${exportVariableName};
`;
}
}
export interface ICreationStrategyCommonJsStringOptions extends ICreationStrategyCommonJsOptions {
/**
* If the exported instance should be exposed as a function, which accepts an optional hash of variables.
* If this is true, variables will be extracted from the `variables` hash.
*/
asFunction?: boolean;
}