UNPKG

15 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var path = require('path');
6var util = require('util');
7var fs = require('fs');
8
9function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
10
11var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
12var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
13
14const readFile = util.promisify(fs__default['default'].readFile);
15/**
16 * Send a string to lowercase
17 * @param str the string to lowercase
18 * @returns the lowercased string
19 */
20const toLowerCase = (str) => str.toLowerCase();
21/**
22 * Convert a string using dash-case to PascalCase
23 * @param str the string to convert to PascalCase
24 * @returns the PascalCased string
25 */
26const dashToPascalCase = (str) => toLowerCase(str)
27 .split('-')
28 .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
29 .join('');
30/**
31 * Sorts a provided array by a property belonging to an item that exists on each item in the array
32 * @param array the array to sort
33 * @param prop a function to look up a field on an entry in the provided array
34 * @returns a shallow copy of the array, sorted by the property resolved by `prop`
35 */
36function sortBy(array, prop) {
37 return array.slice().sort((a, b) => {
38 const nameA = prop(a);
39 const nameB = prop(b);
40 if (nameA < nameB)
41 return -1;
42 if (nameA > nameB)
43 return 1;
44 return 0;
45 });
46}
47/**
48 * Normalize a path
49 * @param str the path to normalize
50 * @returns the normalized path
51 */
52function normalizePath(str) {
53 // Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar
54 // https://github.com/sindresorhus/slash MIT
55 // By Sindre Sorhus
56 if (typeof str !== 'string') {
57 throw new Error(`invalid path to normalize`);
58 }
59 str = str.trim();
60 if (EXTENDED_PATH_REGEX.test(str) || NON_ASCII_REGEX.test(str)) {
61 return str;
62 }
63 str = str.replace(SLASH_REGEX, '/');
64 // always remove the trailing /
65 // this makes our file cache look ups consistent
66 if (str.charAt(str.length - 1) === '/') {
67 const colonIndex = str.indexOf(':');
68 if (colonIndex > -1) {
69 if (colonIndex < str.length - 2) {
70 str = str.substring(0, str.length - 1);
71 }
72 }
73 else if (str.length > 1) {
74 str = str.substring(0, str.length - 1);
75 }
76 }
77 return str;
78}
79/**
80 * Generate the relative import from `pathFrom` to `pathTo`
81 * @param pathFrom the path that shall be used as the origin in determining the relative path
82 * @param pathTo the path that shall be used as the destination in determining the relative path
83 * @param ext an extension to remove from the final path
84 * @returns the derived relative import
85 */
86function relativeImport(pathFrom, pathTo, ext) {
87 let relativePath = path__default['default'].relative(path__default['default'].dirname(pathFrom), path__default['default'].dirname(pathTo));
88 if (relativePath === '') {
89 relativePath = '.';
90 }
91 else if (relativePath[0] !== '.') {
92 relativePath = './' + relativePath;
93 }
94 return normalizePath(`${relativePath}/${path__default['default'].basename(pathTo, ext)}`);
95}
96/**
97 * Attempts to read a `package.json` file at the provided directory.
98 * @param rootDir the directory to search for the `package.json` file to read
99 * @returns the read and parsed `package.json` file
100 */
101async function readPackageJson(rootDir) {
102 const pkgJsonPath = path__default['default'].join(rootDir, 'package.json');
103 let pkgJson;
104 try {
105 pkgJson = await readFile(pkgJsonPath, 'utf8');
106 }
107 catch (e) {
108 throw new Error(`Missing "package.json" file for distribution: ${pkgJsonPath}`);
109 }
110 let pkgData;
111 try {
112 pkgData = JSON.parse(pkgJson);
113 }
114 catch (e) {
115 throw new Error(`Error parsing package.json: ${pkgJsonPath}, ${e}`);
116 }
117 return pkgData;
118}
119const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
120const NON_ASCII_REGEX = /[^\x00-\x80]+/;
121const SLASH_REGEX = /\\/g;
122
123/**
124 * Generate and write the Stencil-React bindings to disc
125 * @param config the Stencil configuration associated with the project
126 * @param compilerCtx the compiler context of the current Stencil build
127 * @param outputTarget the output target configuration for generating the React wrapper
128 * @param components the components to generate the bindings for
129 */
130async function reactProxyOutput(config, compilerCtx, outputTarget, components) {
131 const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
132 const rootDir = config.rootDir;
133 const pkgData = await readPackageJson(rootDir);
134 const finalText = generateProxies(config, filteredComponents, pkgData, outputTarget, rootDir);
135 await compilerCtx.fs.writeFile(outputTarget.proxiesFile, finalText);
136 await copyResources(config, outputTarget);
137}
138/**
139 * Removes all components from the provided `cmps` list that exist in the provided `excludedComponents` list
140 * @param excludeComponents the list of components that should be removed from the provided `cmps` list
141 * @param cmps a list of components
142 * @returns the filtered list of components
143 */
144function getFilteredComponents(excludeComponents = [], cmps) {
145 return sortBy(cmps, (cmp) => cmp.tagName).filter((c) => !excludeComponents.includes(c.tagName) && !c.internal);
146}
147/**
148 * Generate the code that will be responsible for creating the Stencil-React bindings
149 * @param config the Stencil configuration associated with the project
150 * @param components the Stencil components to generate wrappers for
151 * @param pkgData `package.json` data for the Stencil project
152 * @param outputTarget the output target configuration used to generate the Stencil-React bindings
153 * @param rootDir the directory of the Stencil project
154 * @returns the generated code to create the Stencil-React bindings
155 */
156function generateProxies(config, components, pkgData, outputTarget, rootDir) {
157 const distTypesDir = path__default['default'].dirname(pkgData.types);
158 const dtsFilePath = path__default['default'].join(rootDir, distTypesDir, GENERATED_DTS);
159 const componentsTypeFile = relativeImport(outputTarget.proxiesFile, dtsFilePath, '.d.ts');
160 const pathToCorePackageLoader = getPathToCorePackageLoader(config, outputTarget);
161 const imports = `/* eslint-disable */
162/* tslint:disable */
163/* auto-generated react proxies */
164import { createReactComponent } from './react-component-lib';\n`;
165 /**
166 * Generate JSX import type from correct location.
167 * When using custom elements build, we need to import from
168 * either the "components" directory or customElementsDir
169 * otherwise we risk bundlers pulling in lazy loaded imports.
170 */
171 const generateTypeImports = () => {
172 if (outputTarget.componentCorePackage !== undefined) {
173 const dirPath = outputTarget.includeImportCustomElements ? `/${outputTarget.customElementsDir || 'components'}` : '';
174 return `import type { ${IMPORT_TYPES} } from '${normalizePath(outputTarget.componentCorePackage)}${dirPath}';\n`;
175 }
176 return `import type { ${IMPORT_TYPES} } from '${normalizePath(componentsTypeFile)}';\n`;
177 };
178 const typeImports = generateTypeImports();
179 let sourceImports = '';
180 let registerCustomElements = '';
181 /**
182 * Build an array of Custom Elements build imports and namespace them so that they do not conflict with the React
183 * wrapper names. For example, IonButton would be imported as IonButtonCmp to not conflict with the IonButton React
184 * Component that takes in the Web Component as a parameter.
185 */
186 if (outputTarget.includeImportCustomElements && outputTarget.componentCorePackage !== undefined) {
187 const cmpImports = components.map(component => {
188 const pascalImport = dashToPascalCase(component.tagName);
189 return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir ||
190 'components'}/${component.tagName}.js';`;
191 });
192 sourceImports = cmpImports.join('\n');
193 }
194 else if (outputTarget.includePolyfills && outputTarget.includeDefineCustomElements) {
195 sourceImports = `import { ${APPLY_POLYFILLS}, ${REGISTER_CUSTOM_ELEMENTS} } from '${pathToCorePackageLoader}';\n`;
196 registerCustomElements = `${APPLY_POLYFILLS}().then(() => ${REGISTER_CUSTOM_ELEMENTS}());`;
197 }
198 else if (!outputTarget.includePolyfills && outputTarget.includeDefineCustomElements) {
199 sourceImports = `import { ${REGISTER_CUSTOM_ELEMENTS} } from '${pathToCorePackageLoader}';\n`;
200 registerCustomElements = `${REGISTER_CUSTOM_ELEMENTS}();`;
201 }
202 const final = [
203 imports,
204 typeImports,
205 sourceImports,
206 registerCustomElements,
207 components.map(cmpMeta => createComponentDefinition(cmpMeta, outputTarget.includeImportCustomElements)).join('\n'),
208 ];
209 return final.join('\n') + '\n';
210}
211/**
212 * Defines the React component that developers will import to use in their applications.
213 * @param cmpMeta Meta data for a single Web Component
214 * @param includeCustomElement If `true`, the Web Component instance will be passed in to createReactComponent to be
215 * registered with the Custom Elements Registry.
216 * @returns An array where each entry is a string version of the React component definition.
217 */
218function createComponentDefinition(cmpMeta, includeCustomElement = false) {
219 const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
220 let template = `export const ${tagNameAsPascal} = /*@__PURE__*/createReactComponent<${IMPORT_TYPES}.${tagNameAsPascal}, HTML${tagNameAsPascal}Element>('${cmpMeta.tagName}'`;
221 if (includeCustomElement) {
222 template += `, undefined, undefined, define${tagNameAsPascal}`;
223 }
224 template += `);`;
225 return [
226 template
227 ];
228}
229/**
230 * Copy resources used to generate the Stencil-React bindings. The resources copied here are not specific a project's
231 * Stencil components, but rather the logic used to do the actual component generation.
232 * @param config the Stencil configuration associated with the project
233 * @param outputTarget the output target configuration for generating the Stencil-React bindings
234 * @returns The results of performing the copy
235 */
236async function copyResources(config, outputTarget) {
237 if (!config.sys || !config.sys.copy || !config.sys.glob) {
238 throw new Error('stencil is not properly initialized at this step. Notify the developer');
239 }
240 const srcDirectory = path__default['default'].join(__dirname, '..', 'react-component-lib');
241 const destDirectory = path__default['default'].join(path__default['default'].dirname(outputTarget.proxiesFile), 'react-component-lib');
242 return config.sys.copy([
243 {
244 src: srcDirectory,
245 dest: destDirectory,
246 keepDirStructure: false,
247 warn: false,
248 },
249 ], srcDirectory);
250}
251/**
252 * Derive the path to the loader
253 * @param config the Stencil configuration for the project
254 * @param outputTarget the output target used for generating the Stencil-React bindings
255 * @returns the derived loader path
256 */
257function getPathToCorePackageLoader(config, outputTarget) {
258 var _a;
259 const basePkg = outputTarget.componentCorePackage || '';
260 const distOutputTarget = (_a = config.outputTargets) === null || _a === void 0 ? void 0 : _a.find((o) => o.type === 'dist');
261 const distAbsEsmLoaderPath = (distOutputTarget === null || distOutputTarget === void 0 ? void 0 : distOutputTarget.esmLoaderPath) && path__default['default'].isAbsolute(distOutputTarget.esmLoaderPath)
262 ? distOutputTarget.esmLoaderPath
263 : null;
264 const distRelEsmLoaderPath = config.rootDir && distAbsEsmLoaderPath
265 ? path__default['default'].relative(config.rootDir, distAbsEsmLoaderPath)
266 : null;
267 const loaderDir = outputTarget.loaderDir || distRelEsmLoaderPath || DEFAULT_LOADER_DIR;
268 return normalizePath(path__default['default'].join(basePkg, loaderDir));
269}
270const GENERATED_DTS = 'components.d.ts';
271const IMPORT_TYPES = 'JSX';
272const REGISTER_CUSTOM_ELEMENTS = 'defineCustomElements';
273const APPLY_POLYFILLS = 'applyPolyfills';
274const DEFAULT_LOADER_DIR = '/dist/loader';
275
276/**
277 * Creates an output target for binding Stencil components to be used in a React context
278 * @param outputTarget the user-defined output target defined in a Stencil configuration file
279 * @returns an output target that can be used by the Stencil compiler
280 */
281const reactOutputTarget = (outputTarget) => ({
282 type: 'custom',
283 name: 'react-library',
284 validate(config) {
285 return normalizeOutputTarget(config, outputTarget);
286 },
287 async generator(config, compilerCtx, buildCtx) {
288 const timespan = buildCtx.createTimeSpan(`generate react started`, true);
289 await reactProxyOutput(config, compilerCtx, outputTarget, buildCtx.components);
290 timespan.finish(`generate react finished`);
291 },
292});
293/**
294 * Normalizes the structure of a provided output target and verifies a Stencil configuration
295 * associated with the wrapper is valid
296 * @param config the configuration to validate
297 * @param outputTarget the output target to normalize
298 * @returns an output target that's been normalized
299 */
300function normalizeOutputTarget(config, outputTarget) {
301 var _a, _b;
302 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 });
303 if (config.rootDir == null) {
304 throw new Error('rootDir is not set and it should be set by stencil itself');
305 }
306 if (outputTarget.proxiesFile == null) {
307 throw new Error('proxiesFile is required');
308 }
309 if (outputTarget.includeDefineCustomElements && outputTarget.includeImportCustomElements) {
310 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.');
311 }
312 if (outputTarget.includeImportCustomElements && outputTarget.includePolyfills) {
313 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.');
314 }
315 if (outputTarget.directivesProxyFile && !path__default['default'].isAbsolute(outputTarget.directivesProxyFile)) {
316 results.proxiesFile = normalizePath(path__default['default'].join(config.rootDir, outputTarget.proxiesFile));
317 }
318 return results;
319}
320
321exports.reactOutputTarget = reactOutputTarget;