UNPKG

14.4 kBJavaScriptView Raw
1import path from 'path';
2import { promisify } from 'util';
3import fs from 'fs';
4
5const readFile = promisify(fs.readFile);
6/**
7 * Send a string to lowercase
8 * @param str the string to lowercase
9 * @returns the lowercased string
10 */
11const toLowerCase = (str) => str.toLowerCase();
12/**
13 * Convert a string using dash-case to PascalCase
14 * @param str the string to convert to PascalCase
15 * @returns the PascalCased string
16 */
17const dashToPascalCase = (str) => toLowerCase(str)
18 .split('-')
19 .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
20 .join('');
21/**
22 * Sorts a provided array by a property belonging to an item that exists on each item in the array
23 * @param array the array to sort
24 * @param prop a function to look up a field on an entry in the provided array
25 * @returns a shallow copy of the array, sorted by the property resolved by `prop`
26 */
27function 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 * Normalize a path
40 * @param str the path to normalize
41 * @returns the normalized path
42 */
43function normalizePath(str) {
44 // Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar
45 // https://github.com/sindresorhus/slash MIT
46 // By Sindre Sorhus
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 // always remove the trailing /
56 // this makes our file cache look ups consistent
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 * Generate the relative import from `pathFrom` to `pathTo`
72 * @param pathFrom the path that shall be used as the origin in determining the relative path
73 * @param pathTo the path that shall be used as the destination in determining the relative path
74 * @param ext an extension to remove from the final path
75 * @returns the derived relative import
76 */
77function 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 * Attempts to read a `package.json` file at the provided directory.
89 * @param rootDir the directory to search for the `package.json` file to read
90 * @returns the read and parsed `package.json` file
91 */
92async 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}
110const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
111const NON_ASCII_REGEX = /[^\x00-\x80]+/;
112const SLASH_REGEX = /\\/g;
113
114/**
115 * Generate and write the Stencil-React bindings to disc
116 * @param config the Stencil configuration associated with the project
117 * @param compilerCtx the compiler context of the current Stencil build
118 * @param outputTarget the output target configuration for generating the React wrapper
119 * @param components the components to generate the bindings for
120 */
121async 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 * Removes all components from the provided `cmps` list that exist in the provided `excludedComponents` list
131 * @param excludeComponents the list of components that should be removed from the provided `cmps` list
132 * @param cmps a list of components
133 * @returns the filtered list of components
134 */
135function getFilteredComponents(excludeComponents = [], cmps) {
136 return sortBy(cmps, (cmp) => cmp.tagName).filter((c) => !excludeComponents.includes(c.tagName) && !c.internal);
137}
138/**
139 * Generate the code that will be responsible for creating the Stencil-React bindings
140 * @param config the Stencil configuration associated with the project
141 * @param components the Stencil components to generate wrappers for
142 * @param pkgData `package.json` data for the Stencil project
143 * @param outputTarget the output target configuration used to generate the Stencil-React bindings
144 * @param rootDir the directory of the Stencil project
145 * @returns the generated code to create the Stencil-React bindings
146 */
147function 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 */
155import { createReactComponent } from './react-component-lib';\n`;
156 /**
157 * Generate JSX import type from correct location.
158 * When using custom elements build, we need to import from
159 * either the "components" directory or customElementsDir
160 * otherwise we risk bundlers pulling in lazy loaded imports.
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 * Build an array of Custom Elements build imports and namespace them so that they do not conflict with the React
174 * wrapper names. For example, IonButton would be imported as IonButtonCmp to not conflict with the IonButton React
175 * Component that takes in the Web Component as a parameter.
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 * Defines the React component that developers will import to use in their applications.
204 * @param cmpMeta Meta data for a single Web Component
205 * @param includeCustomElement If `true`, the Web Component instance will be passed in to createReactComponent to be
206 * registered with the Custom Elements Registry.
207 * @returns An array where each entry is a string version of the React component definition.
208 */
209function 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 * Copy resources used to generate the Stencil-React bindings. The resources copied here are not specific a project's
222 * Stencil components, but rather the logic used to do the actual component generation.
223 * @param config the Stencil configuration associated with the project
224 * @param outputTarget the output target configuration for generating the Stencil-React bindings
225 * @returns The results of performing the copy
226 */
227async 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 * Derive the path to the loader
244 * @param config the Stencil configuration for the project
245 * @param outputTarget the output target used for generating the Stencil-React bindings
246 * @returns the derived loader path
247 */
248function 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}
261const GENERATED_DTS = 'components.d.ts';
262const IMPORT_TYPES = 'JSX';
263const REGISTER_CUSTOM_ELEMENTS = 'defineCustomElements';
264const APPLY_POLYFILLS = 'applyPolyfills';
265const DEFAULT_LOADER_DIR = '/dist/loader';
266
267/**
268 * Creates an output target for binding Stencil components to be used in a React context
269 * @param outputTarget the user-defined output target defined in a Stencil configuration file
270 * @returns an output target that can be used by the Stencil compiler
271 */
272const 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 * Normalizes the structure of a provided output target and verifies a Stencil configuration
286 * associated with the wrapper is valid
287 * @param config the configuration to validate
288 * @param outputTarget the output target to normalize
289 * @returns an output target that's been normalized
290 */
291function 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
312export { reactOutputTarget };