1 | import path from 'path';
|
2 | import { promisify } from 'util';
|
3 | import fs from 'fs';
|
4 |
|
5 | const readFile = promisify(fs.readFile);
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | const toLowerCase = (str) => str.toLowerCase();
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const dashToPascalCase = (str) => toLowerCase(str)
|
18 | .split('-')
|
19 | .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
20 | .join('');
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | function sortBy(array, prop) {
|
28 | return array.slice().sort((a, b) => {
|
29 | const nameA = prop(a);
|
30 | const nameB = prop(b);
|
31 | if (nameA < nameB)
|
32 | return -1;
|
33 | if (nameA > nameB)
|
34 | return 1;
|
35 | return 0;
|
36 | });
|
37 | }
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | function normalizePath(str) {
|
44 |
|
45 |
|
46 |
|
47 | if (typeof str !== 'string') {
|
48 | throw new Error(`invalid path to normalize`);
|
49 | }
|
50 | str = str.trim();
|
51 | if (EXTENDED_PATH_REGEX.test(str) || NON_ASCII_REGEX.test(str)) {
|
52 | return str;
|
53 | }
|
54 | str = str.replace(SLASH_REGEX, '/');
|
55 |
|
56 |
|
57 | if (str.charAt(str.length - 1) === '/') {
|
58 | const colonIndex = str.indexOf(':');
|
59 | if (colonIndex > -1) {
|
60 | if (colonIndex < str.length - 2) {
|
61 | str = str.substring(0, str.length - 1);
|
62 | }
|
63 | }
|
64 | else if (str.length > 1) {
|
65 | str = str.substring(0, str.length - 1);
|
66 | }
|
67 | }
|
68 | return str;
|
69 | }
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | function relativeImport(pathFrom, pathTo, ext) {
|
78 | let relativePath = path.relative(path.dirname(pathFrom), path.dirname(pathTo));
|
79 | if (relativePath === '') {
|
80 | relativePath = '.';
|
81 | }
|
82 | else if (relativePath[0] !== '.') {
|
83 | relativePath = './' + relativePath;
|
84 | }
|
85 | return normalizePath(`${relativePath}/${path.basename(pathTo, ext)}`);
|
86 | }
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | async function readPackageJson(rootDir) {
|
93 | const pkgJsonPath = path.join(rootDir, 'package.json');
|
94 | let pkgJson;
|
95 | try {
|
96 | pkgJson = await readFile(pkgJsonPath, 'utf8');
|
97 | }
|
98 | catch (e) {
|
99 | throw new Error(`Missing "package.json" file for distribution: ${pkgJsonPath}`);
|
100 | }
|
101 | let pkgData;
|
102 | try {
|
103 | pkgData = JSON.parse(pkgJson);
|
104 | }
|
105 | catch (e) {
|
106 | throw new Error(`Error parsing package.json: ${pkgJsonPath}, ${e}`);
|
107 | }
|
108 | return pkgData;
|
109 | }
|
110 | const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
|
111 | const NON_ASCII_REGEX = /[^\x00-\x80]+/;
|
112 | const SLASH_REGEX = /\\/g;
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | async function reactProxyOutput(config, compilerCtx, outputTarget, components) {
|
122 | const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
|
123 | const rootDir = config.rootDir;
|
124 | const pkgData = await readPackageJson(rootDir);
|
125 | const finalText = generateProxies(config, filteredComponents, pkgData, outputTarget, rootDir);
|
126 | await compilerCtx.fs.writeFile(outputTarget.proxiesFile, finalText);
|
127 | await copyResources(config, outputTarget);
|
128 | }
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | function getFilteredComponents(excludeComponents = [], cmps) {
|
136 | return sortBy(cmps, (cmp) => cmp.tagName).filter((c) => !excludeComponents.includes(c.tagName) && !c.internal);
|
137 | }
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 | function generateProxies(config, components, pkgData, outputTarget, rootDir) {
|
148 | const distTypesDir = path.dirname(pkgData.types);
|
149 | const dtsFilePath = path.join(rootDir, distTypesDir, GENERATED_DTS);
|
150 | const componentsTypeFile = relativeImport(outputTarget.proxiesFile, dtsFilePath, '.d.ts');
|
151 | const pathToCorePackageLoader = getPathToCorePackageLoader(config, outputTarget);
|
152 | const imports = `/* eslint-disable */
|
153 | /* tslint:disable */
|
154 | /* auto-generated react proxies */
|
155 | import { createReactComponent } from './react-component-lib';\n`;
|
156 | |
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | const generateTypeImports = () => {
|
163 | if (outputTarget.componentCorePackage !== undefined) {
|
164 | const dirPath = outputTarget.includeImportCustomElements ? `/${outputTarget.customElementsDir || 'components'}` : '';
|
165 | return `import type { ${IMPORT_TYPES} } from '${normalizePath(outputTarget.componentCorePackage)}${dirPath}';\n`;
|
166 | }
|
167 | return `import type { ${IMPORT_TYPES} } from '${normalizePath(componentsTypeFile)}';\n`;
|
168 | };
|
169 | const typeImports = generateTypeImports();
|
170 | let sourceImports = '';
|
171 | let registerCustomElements = '';
|
172 | |
173 |
|
174 |
|
175 |
|
176 |
|
177 | if (outputTarget.includeImportCustomElements && outputTarget.componentCorePackage !== undefined) {
|
178 | const cmpImports = components.map(component => {
|
179 | const pascalImport = dashToPascalCase(component.tagName);
|
180 | return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir ||
|
181 | 'components'}/${component.tagName}.js';`;
|
182 | });
|
183 | sourceImports = cmpImports.join('\n');
|
184 | }
|
185 | else if (outputTarget.includePolyfills && outputTarget.includeDefineCustomElements) {
|
186 | sourceImports = `import { ${APPLY_POLYFILLS}, ${REGISTER_CUSTOM_ELEMENTS} } from '${pathToCorePackageLoader}';\n`;
|
187 | registerCustomElements = `${APPLY_POLYFILLS}().then(() => ${REGISTER_CUSTOM_ELEMENTS}());`;
|
188 | }
|
189 | else if (!outputTarget.includePolyfills && outputTarget.includeDefineCustomElements) {
|
190 | sourceImports = `import { ${REGISTER_CUSTOM_ELEMENTS} } from '${pathToCorePackageLoader}';\n`;
|
191 | registerCustomElements = `${REGISTER_CUSTOM_ELEMENTS}();`;
|
192 | }
|
193 | const final = [
|
194 | imports,
|
195 | typeImports,
|
196 | sourceImports,
|
197 | registerCustomElements,
|
198 | components.map(cmpMeta => createComponentDefinition(cmpMeta, outputTarget.includeImportCustomElements)).join('\n'),
|
199 | ];
|
200 | return final.join('\n') + '\n';
|
201 | }
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 | function createComponentDefinition(cmpMeta, includeCustomElement = false) {
|
210 | const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
|
211 | let template = `export const ${tagNameAsPascal} = /*@__PURE__*/createReactComponent<${IMPORT_TYPES}.${tagNameAsPascal}, HTML${tagNameAsPascal}Element>('${cmpMeta.tagName}'`;
|
212 | if (includeCustomElement) {
|
213 | template += `, undefined, undefined, define${tagNameAsPascal}`;
|
214 | }
|
215 | template += `);`;
|
216 | return [
|
217 | template
|
218 | ];
|
219 | }
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | async function copyResources(config, outputTarget) {
|
228 | if (!config.sys || !config.sys.copy || !config.sys.glob) {
|
229 | throw new Error('stencil is not properly initialized at this step. Notify the developer');
|
230 | }
|
231 | const srcDirectory = path.join(__dirname, '..', 'react-component-lib');
|
232 | const destDirectory = path.join(path.dirname(outputTarget.proxiesFile), 'react-component-lib');
|
233 | return config.sys.copy([
|
234 | {
|
235 | src: srcDirectory,
|
236 | dest: destDirectory,
|
237 | keepDirStructure: false,
|
238 | warn: false,
|
239 | },
|
240 | ], srcDirectory);
|
241 | }
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 | function getPathToCorePackageLoader(config, outputTarget) {
|
249 | var _a;
|
250 | const basePkg = outputTarget.componentCorePackage || '';
|
251 | const distOutputTarget = (_a = config.outputTargets) === null || _a === void 0 ? void 0 : _a.find((o) => o.type === 'dist');
|
252 | const distAbsEsmLoaderPath = (distOutputTarget === null || distOutputTarget === void 0 ? void 0 : distOutputTarget.esmLoaderPath) && path.isAbsolute(distOutputTarget.esmLoaderPath)
|
253 | ? distOutputTarget.esmLoaderPath
|
254 | : null;
|
255 | const distRelEsmLoaderPath = config.rootDir && distAbsEsmLoaderPath
|
256 | ? path.relative(config.rootDir, distAbsEsmLoaderPath)
|
257 | : null;
|
258 | const loaderDir = outputTarget.loaderDir || distRelEsmLoaderPath || DEFAULT_LOADER_DIR;
|
259 | return normalizePath(path.join(basePkg, loaderDir));
|
260 | }
|
261 | const GENERATED_DTS = 'components.d.ts';
|
262 | const IMPORT_TYPES = 'JSX';
|
263 | const REGISTER_CUSTOM_ELEMENTS = 'defineCustomElements';
|
264 | const APPLY_POLYFILLS = 'applyPolyfills';
|
265 | const DEFAULT_LOADER_DIR = '/dist/loader';
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 | const reactOutputTarget = (outputTarget) => ({
|
273 | type: 'custom',
|
274 | name: 'react-library',
|
275 | validate(config) {
|
276 | return normalizeOutputTarget(config, outputTarget);
|
277 | },
|
278 | async generator(config, compilerCtx, buildCtx) {
|
279 | const timespan = buildCtx.createTimeSpan(`generate react started`, true);
|
280 | await reactProxyOutput(config, compilerCtx, outputTarget, buildCtx.components);
|
281 | timespan.finish(`generate react finished`);
|
282 | },
|
283 | });
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 | function normalizeOutputTarget(config, outputTarget) {
|
292 | var _a, _b;
|
293 | const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], includePolyfills: (_a = outputTarget.includePolyfills) !== null && _a !== void 0 ? _a : true, includeDefineCustomElements: (_b = outputTarget.includeDefineCustomElements) !== null && _b !== void 0 ? _b : true });
|
294 | if (config.rootDir == null) {
|
295 | throw new Error('rootDir is not set and it should be set by stencil itself');
|
296 | }
|
297 | if (outputTarget.proxiesFile == null) {
|
298 | throw new Error('proxiesFile is required');
|
299 | }
|
300 | if (outputTarget.includeDefineCustomElements && outputTarget.includeImportCustomElements) {
|
301 | throw new Error('includeDefineCustomElements cannot be used at the same time as includeImportCustomElements since includeDefineCustomElements is used for lazy loading components. Set `includeDefineCustomElements: false` in your React output target config to resolve this.');
|
302 | }
|
303 | if (outputTarget.includeImportCustomElements && outputTarget.includePolyfills) {
|
304 | throw new Error('includePolyfills cannot be used at the same time as includeImportCustomElements. Set `includePolyfills: false` in your React output target config to resolve this.');
|
305 | }
|
306 | if (outputTarget.directivesProxyFile && !path.isAbsolute(outputTarget.directivesProxyFile)) {
|
307 | results.proxiesFile = normalizePath(path.join(config.rootDir, outputTarget.proxiesFile));
|
308 | }
|
309 | return results;
|
310 | }
|
311 |
|
312 | export { reactOutputTarget };
|