UNPKG

24.7 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var path = require('path');
6var os = require('os');
7
8function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
10var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
11
12const toLowerCase = (str) => str.toLowerCase();
13const dashToPascalCase = (str) => toLowerCase(str)
14 .split('-')
15 .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
16 .join('');
17function sortBy(array, prop) {
18 return array.slice().sort((a, b) => {
19 const nameA = prop(a);
20 const nameB = prop(b);
21 if (nameA < nameB)
22 return -1;
23 if (nameA > nameB)
24 return 1;
25 return 0;
26 });
27}
28function normalizePath(str) {
29 // Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar
30 // https://github.com/sindresorhus/slash MIT
31 // By Sindre Sorhus
32 if (typeof str !== 'string') {
33 throw new Error(`invalid path to normalize`);
34 }
35 str = str.trim();
36 if (EXTENDED_PATH_REGEX.test(str) || NON_ASCII_REGEX.test(str)) {
37 return str;
38 }
39 str = str.replace(SLASH_REGEX, '/');
40 // always remove the trailing /
41 // this makes our file cache look ups consistent
42 if (str.charAt(str.length - 1) === '/') {
43 const colonIndex = str.indexOf(':');
44 if (colonIndex > -1) {
45 if (colonIndex < str.length - 2) {
46 str = str.substring(0, str.length - 1);
47 }
48 }
49 else if (str.length > 1) {
50 str = str.substring(0, str.length - 1);
51 }
52 }
53 return str;
54}
55function relativeImport(pathFrom, pathTo, ext) {
56 let relativePath = path__default['default'].relative(path__default['default'].dirname(pathFrom), path__default['default'].dirname(pathTo));
57 if (relativePath === '') {
58 relativePath = '.';
59 }
60 else if (relativePath[0] !== '.') {
61 relativePath = './' + relativePath;
62 }
63 return normalizePath(`${relativePath}/${path__default['default'].basename(pathTo, ext)}`);
64}
65async function readPackageJson(config, rootDir) {
66 var _a;
67 const pkgJsonPath = path__default['default'].join(rootDir, 'package.json');
68 let pkgJson;
69 try {
70 pkgJson = (await ((_a = config.sys) === null || _a === void 0 ? void 0 : _a.readFile(pkgJsonPath, 'utf8')));
71 }
72 catch (e) {
73 throw new Error(`Missing "package.json" file for distribution: ${pkgJsonPath}`);
74 }
75 let pkgData;
76 try {
77 pkgData = JSON.parse(pkgJson);
78 }
79 catch (e) {
80 throw new Error(`Error parsing package.json: ${pkgJsonPath}, ${e}`);
81 }
82 return pkgData;
83}
84/**
85 * Formats an array of strings to a string of quoted, comma separated values.
86 * @param list The list of unformatted strings to format
87 * @returns The formatted array of strings. (e.g. ['foo', 'bar']) => `'foo', 'bar'`
88 */
89const formatToQuotedList = (list) => list.map((item) => `'${item}'`).join(', ');
90/**
91 * Creates an import statement for a list of named imports from a module.
92 * @param imports The list of named imports.
93 * @param module The module to import from.
94 *
95 * @returns The import statement as a string.
96 */
97const createImportStatement = (imports, module) => {
98 if (imports.length === 0) {
99 return '';
100 }
101 return `import { ${imports.join(', ')} } from '${module}';`;
102};
103/**
104 * Creates the collection of import statements for a component based on the component's events type dependencies.
105 * @param componentTagName The tag name of the component (pascal case).
106 * @param events The events compiler metadata.
107 * @param options The options for generating the import statements (e.g. whether to import from the custom elements directory).
108 * @returns The import statements as an array of strings.
109 */
110const createComponentEventTypeImports = (componentTagName, events, options) => {
111 const { componentCorePackage, includeImportCustomElements, customElementsDir } = options;
112 const imports = [];
113 const namedImports = new Set();
114 const importPathName = normalizePath(componentCorePackage) + (includeImportCustomElements ? `/${customElementsDir || 'components'}` : '');
115 events.forEach((event) => {
116 Object.entries(event.complexType.references).forEach(([typeName, refObject]) => {
117 if (refObject.location === 'local' || refObject.location === 'import') {
118 const newTypeName = `I${componentTagName}${typeName}`;
119 // Prevents duplicate imports for the same type.
120 if (!namedImports.has(newTypeName)) {
121 imports.push(`import type { ${typeName} as ${newTypeName} } from '${importPathName}';`);
122 namedImports.add(newTypeName);
123 }
124 }
125 });
126 });
127 return imports.join('\n');
128};
129const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
130const NON_ASCII_REGEX = /[^\x00-\x80]+/;
131const SLASH_REGEX = /\\/g;
132
133/**
134 * Creates an Angular component declaration from formatted Stencil compiler metadata.
135 *
136 * @param tagName The tag name of the component.
137 * @param inputs The inputs of the Stencil component (e.g. ['myInput']).
138 * @param outputs The outputs/events of the Stencil component. (e.g. ['myOutput']).
139 * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
140 * @param includeImportCustomElements Whether to define the component as a custom element.
141 * @returns The component declaration as a string.
142 */
143const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false) => {
144 const tagNameAsPascal = dashToPascalCase(tagName);
145 const hasInputs = inputs.length > 0;
146 const hasOutputs = outputs.length > 0;
147 const hasMethods = methods.length > 0;
148 // Formats the input strings into comma separated, single quoted values.
149 const formattedInputs = formatToQuotedList(inputs);
150 // Formats the output strings into comma separated, single quoted values.
151 const formattedOutputs = formatToQuotedList(outputs);
152 // Formats the method strings into comma separated, single quoted values.
153 const formattedMethods = formatToQuotedList(methods);
154 const proxyCmpOptions = [];
155 if (includeImportCustomElements) {
156 const defineCustomElementFn = `define${tagNameAsPascal}`;
157 proxyCmpOptions.push(`\n defineCustomElementFn: ${defineCustomElementFn}`);
158 }
159 if (hasInputs) {
160 proxyCmpOptions.push(`\n inputs: [${formattedInputs}]`);
161 }
162 if (hasMethods) {
163 proxyCmpOptions.push(`\n methods: [${formattedMethods}]`);
164 }
165 /**
166 * Notes on the generated output:
167 * - We disable @angular-eslint/no-inputs-metadata-property, so that
168 * Angular does not complain about the inputs property. The output target
169 * uses the inputs property to define the inputs of the component instead of
170 * having to use the @Input decorator (and manually define the type and default value).
171 */
172 const output = `@ProxyCmp({${proxyCmpOptions.join(',')}\n})
173@Component({
174 selector: '${tagName}',
175 changeDetection: ChangeDetectionStrategy.OnPush,
176 template: '<ng-content></ng-content>',
177 // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
178 inputs: [${formattedInputs}],
179})
180export class ${tagNameAsPascal} {
181 protected el: HTMLElement;
182 constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
183 c.detach();
184 this.el = r.nativeElement;${hasOutputs
185 ? `
186 proxyOutputs(this, this.el, [${formattedOutputs}]);`
187 : ''}
188 }
189}`;
190 return output;
191};
192/**
193 * Sanitizes and formats the component event type.
194 * @param componentClassName The class name of the component (e.g. 'MyComponent')
195 * @param event The Stencil component event.
196 * @returns The sanitized event type as a string.
197 */
198const formatOutputType = (componentClassName, event) => {
199 /**
200 * The original attribute contains the original type defined by the devs.
201 * This regexp normalizes the reference, by removing linebreaks,
202 * replacing consecutive spaces with a single space, and adding a single space after commas.
203 */
204 return Object.entries(event.complexType.references)
205 .filter(([_, refObject]) => refObject.location === 'local' || refObject.location === 'import')
206 .reduce((type, [src, dst]) => {
207 const renamedType = `I${componentClassName}${type}`;
208 return (renamedType
209 .replace(new RegExp(`^${src}$`, 'g'), `${dst}`)
210 // Capture all instances of the `src` field surrounded by non-word characters on each side and join them.
211 .replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].join('')));
212 }, event.complexType.original
213 .replace(/\n/g, ' ')
214 .replace(/\s{2,}/g, ' ')
215 .replace(/,\s*/g, ', '));
216};
217/**
218 * Creates a formatted comment block based on the JS doc comment.
219 * @param doc The compiler jsdoc.
220 * @returns The formatted comment block as a string.
221 */
222const createDocComment = (doc) => {
223 if (doc.text.trim().length === 0 && doc.tags.length === 0) {
224 return '';
225 }
226 return `/**
227 * ${doc.text}${doc.tags.length > 0 ? ' ' : ''}${doc.tags.map((tag) => `@${tag.name} ${tag.text}`)}
228 */`;
229};
230/**
231 * Creates the component interface type definition.
232 * @param tagNameAsPascal The tag name as PascalCase.
233 * @param events The events to generate the interface properties for.
234 * @param componentCorePackage The component core package.
235 * @param includeImportCustomElements Whether to include the import for the custom element definition.
236 * @param customElementsDir The custom elements directory.
237 * @returns The component interface type definition as a string.
238 */
239const createComponentTypeDefinition = (tagNameAsPascal, events, componentCorePackage, includeImportCustomElements = false, customElementsDir) => {
240 const publicEvents = events.filter((ev) => !ev.internal);
241 const eventTypeImports = createComponentEventTypeImports(tagNameAsPascal, publicEvents, {
242 componentCorePackage,
243 includeImportCustomElements,
244 customElementsDir,
245 });
246 const eventTypes = publicEvents.map((event) => {
247 const comment = createDocComment(event.docs);
248 let eventName = event.name;
249 if (event.name.includes('-')) {
250 // If an event name includes a dash, we need to wrap it in quotes.
251 // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
252 eventName = `'${event.name}'`;
253 }
254 return `${comment.length > 0 ? ` ${comment}` : ''}
255 ${eventName}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`;
256 });
257 const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`;
258 const typeDefinition = (eventTypeImports.length > 0 ? `${eventTypeImports + '\n\n'}` : '') +
259 `${interfaceDeclaration}${eventTypes.length === 0
260 ? '}'
261 : `
262${eventTypes.join('\n')}
263}`}`;
264 return typeDefinition;
265};
266
267function generateAngularDirectivesFile(compilerCtx, components, outputTarget) {
268 // Only create the file if it is defined in the stencil configuration
269 if (!outputTarget.directivesArrayFile) {
270 return Promise.resolve();
271 }
272 const proxyPath = relativeImport(outputTarget.directivesArrayFile, outputTarget.directivesProxyFile, '.ts');
273 const directives = components
274 .map((cmpMeta) => dashToPascalCase(cmpMeta.tagName))
275 .map((className) => `d.${className}`)
276 .join(',\n ');
277 const c = `
278import * as d from '${proxyPath}';
279
280export const DIRECTIVES = [
281 ${directives}
282];
283`;
284 return compilerCtx.fs.writeFile(outputTarget.directivesArrayFile, c);
285}
286
287async function generateValueAccessors(compilerCtx, components, outputTarget, config) {
288 if (!Array.isArray(outputTarget.valueAccessorConfigs) || outputTarget.valueAccessorConfigs.length === 0) {
289 return;
290 }
291 const targetDir = path__default['default'].dirname(outputTarget.directivesProxyFile);
292 const normalizedValueAccessors = outputTarget.valueAccessorConfigs.reduce((allAccessors, va) => {
293 const elementSelectors = Array.isArray(va.elementSelectors) ? va.elementSelectors : [va.elementSelectors];
294 const type = va.type;
295 let allElementSelectors = [];
296 let allEventTargets = [];
297 if (allAccessors.hasOwnProperty(type)) {
298 allElementSelectors = allAccessors[type].elementSelectors;
299 allEventTargets = allAccessors[type].eventTargets;
300 }
301 return Object.assign(Object.assign({}, allAccessors), { [type]: {
302 elementSelectors: allElementSelectors.concat(elementSelectors),
303 eventTargets: allEventTargets.concat([[va.event, va.targetAttr]]),
304 } });
305 }, {});
306 await Promise.all(Object.keys(normalizedValueAccessors).map(async (type) => {
307 const valueAccessorType = type; // Object.keys converts to string
308 const targetFileName = `${type}-value-accessor.ts`;
309 const targetFilePath = path__default['default'].join(targetDir, targetFileName);
310 const srcFilePath = path__default['default'].join(__dirname, '../resources/control-value-accessors/', targetFileName);
311 const srcFileContents = await compilerCtx.fs.readFile(srcFilePath);
312 const finalText = createValueAccessor(srcFileContents, normalizedValueAccessors[valueAccessorType]);
313 await compilerCtx.fs.writeFile(targetFilePath, finalText);
314 }));
315 await copyResources(config, ['value-accessor.ts'], targetDir);
316}
317function createValueAccessor(srcFileContents, valueAccessor) {
318 const hostContents = valueAccessor.eventTargets.map((listItem) => VALUE_ACCESSOR_EVENTTARGETS.replace(VALUE_ACCESSOR_EVENT, listItem[0]).replace(VALUE_ACCESSOR_TARGETATTR, listItem[1]));
319 return srcFileContents
320 .replace(VALUE_ACCESSOR_SELECTORS, valueAccessor.elementSelectors.join(', '))
321 .replace(VALUE_ACCESSOR_EVENTTARGETS, hostContents.join(`,${os.EOL}`));
322}
323function copyResources(config, resourcesFilesToCopy, directory) {
324 if (!config.sys || !config.sys.copy) {
325 throw new Error('stencil is not properly intialized at this step. Notify the developer');
326 }
327 const copyTasks = resourcesFilesToCopy.map((rf) => {
328 return {
329 src: path__default['default'].join(__dirname, '../resources/control-value-accessors/', rf),
330 dest: path__default['default'].join(directory, rf),
331 keepDirStructure: false,
332 warn: false,
333 };
334 });
335 return config.sys.copy(copyTasks, path__default['default'].join(directory));
336}
337const VALUE_ACCESSOR_SELECTORS = `<VALUE_ACCESSOR_SELECTORS>`;
338const VALUE_ACCESSOR_EVENT = `<VALUE_ACCESSOR_EVENT>`;
339const VALUE_ACCESSOR_TARGETATTR = '<VALUE_ACCESSOR_TARGETATTR>';
340const VALUE_ACCESSOR_EVENTTARGETS = ` '(<VALUE_ACCESSOR_EVENT>)': 'handleChangeEvent($event.target.<VALUE_ACCESSOR_TARGETATTR>)'`;
341
342/**
343 * Creates an Angular module declaration for a component wrapper.
344 * @param componentTagName The tag name of the Stencil component.
345 * @returns The Angular module declaration as a string.
346 */
347const generateAngularModuleForComponent = (componentTagName) => {
348 const tagNameAsPascal = dashToPascalCase(componentTagName);
349 const componentClassName = `${tagNameAsPascal}`;
350 const moduleClassName = `${tagNameAsPascal}Module`;
351 const moduleDefinition = `@NgModule({
352 declarations: [${componentClassName}],
353 exports: [${componentClassName}]
354})
355export class ${moduleClassName} { }`;
356 return moduleDefinition;
357};
358
359async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) {
360 const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
361 const rootDir = config.rootDir;
362 const pkgData = await readPackageJson(config, rootDir);
363 const finalText = generateProxies(filteredComponents, pkgData, outputTarget, config.rootDir);
364 await Promise.all([
365 compilerCtx.fs.writeFile(outputTarget.directivesProxyFile, finalText),
366 copyResources$1(config, outputTarget),
367 generateAngularDirectivesFile(compilerCtx, filteredComponents, outputTarget),
368 generateValueAccessors(compilerCtx, filteredComponents, outputTarget, config),
369 ]);
370}
371function getFilteredComponents(excludeComponents = [], cmps) {
372 return sortBy(cmps, (cmp) => cmp.tagName).filter((c) => !excludeComponents.includes(c.tagName) && !c.internal);
373}
374async function copyResources$1(config, outputTarget) {
375 if (!config.sys || !config.sys.copy || !config.sys.glob) {
376 throw new Error('stencil is not properly initialized at this step. Notify the developer');
377 }
378 const srcDirectory = path__default['default'].join(__dirname, '..', 'angular-component-lib');
379 const destDirectory = path__default['default'].join(path__default['default'].dirname(outputTarget.directivesProxyFile), 'angular-component-lib');
380 return config.sys.copy([
381 {
382 src: srcDirectory,
383 dest: destDirectory,
384 keepDirStructure: false,
385 warn: false,
386 },
387 ], srcDirectory);
388}
389function generateProxies(components, pkgData, outputTarget, rootDir) {
390 var _a;
391 const distTypesDir = path__default['default'].dirname(pkgData.types);
392 const dtsFilePath = path__default['default'].join(rootDir, distTypesDir, GENERATED_DTS);
393 const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts');
394 const includeSingleComponentAngularModules = (_a = outputTarget.includeSingleComponentAngularModules) !== null && _a !== void 0 ? _a : false;
395 const includeOutputImports = components.some((component) => component.events.some((event) => !event.internal));
396 /**
397 * The collection of named imports from @angular/core.
398 */
399 const angularCoreImports = ['ChangeDetectionStrategy', 'ChangeDetectorRef', 'Component', 'ElementRef'];
400 if (includeOutputImports) {
401 angularCoreImports.push('EventEmitter');
402 }
403 angularCoreImports.push('NgZone');
404 /**
405 * The collection of named imports from the angular-component-lib/utils.
406 */
407 const componentLibImports = ['ProxyCmp'];
408 if (includeOutputImports) {
409 componentLibImports.push('proxyOutputs');
410 }
411 if (includeSingleComponentAngularModules) {
412 angularCoreImports.push('NgModule');
413 }
414 const imports = `/* tslint:disable */
415/* auto-generated angular directive proxies */
416${createImportStatement(angularCoreImports, '@angular/core')}
417
418${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`;
419 /**
420 * Generate JSX import type from correct location.
421 * When using custom elements build, we need to import from
422 * either the "components" directory or customElementsDir
423 * otherwise we risk bundlers pulling in lazy loaded imports.
424 */
425 const generateTypeImports = () => {
426 let importLocation = outputTarget.componentCorePackage
427 ? normalizePath(outputTarget.componentCorePackage)
428 : normalizePath(componentsTypeFile);
429 importLocation += outputTarget.includeImportCustomElements
430 ? `/${outputTarget.customElementsDir || 'components'}`
431 : '';
432 return `import ${outputTarget.includeImportCustomElements ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`;
433 };
434 const typeImports = generateTypeImports();
435 let sourceImports = '';
436 /**
437 * Build an array of Custom Elements build imports and namespace them
438 * so that they do not conflict with the React wrapper names. For example,
439 * IonButton would be imported as IonButtonCmp so as to not conflict with the
440 * IonButton React Component that takes in the Web Component as a parameter.
441 */
442 if (outputTarget.includeImportCustomElements && outputTarget.componentCorePackage !== undefined) {
443 const cmpImports = components.map((component) => {
444 const pascalImport = dashToPascalCase(component.tagName);
445 return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || 'components'}/${component.tagName}.js';`;
446 });
447 sourceImports = cmpImports.join('\n');
448 }
449 if (includeSingleComponentAngularModules) {
450 // Generating Angular modules is only supported in the dist-custom-elements build
451 if (!outputTarget.includeImportCustomElements) {
452 throw new Error('Generating single component Angular modules requires the "includeImportCustomElements" option to be set to true.');
453 }
454 }
455 const proxyFileOutput = [];
456 const filterInternalProps = (prop) => !prop.internal;
457 const mapPropName = (prop) => prop.name;
458 const { includeImportCustomElements, componentCorePackage, customElementsDir } = outputTarget;
459 for (let cmpMeta of components) {
460 const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
461 const inputs = [];
462 if (cmpMeta.properties) {
463 inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName));
464 }
465 if (cmpMeta.virtualProperties) {
466 inputs.push(...cmpMeta.virtualProperties.map(mapPropName));
467 }
468 inputs.sort();
469 const outputs = [];
470 if (cmpMeta.events) {
471 outputs.push(...cmpMeta.events.filter(filterInternalProps).map(mapPropName));
472 }
473 const methods = [];
474 if (cmpMeta.methods) {
475 methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
476 }
477 /**
478 * For each component, we need to generate:
479 * 1. The @Component decorated class
480 * 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
481 * 3. The component interface (using declaration merging for types).
482 */
483 const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, includeImportCustomElements);
484 const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
485 const componentTypeDefinition = createComponentTypeDefinition(tagNameAsPascal, cmpMeta.events, componentCorePackage, includeImportCustomElements, customElementsDir);
486 proxyFileOutput.push(componentDefinition, '\n');
487 if (includeSingleComponentAngularModules) {
488 proxyFileOutput.push(moduleDefinition, '\n');
489 }
490 proxyFileOutput.push(componentTypeDefinition, '\n');
491 }
492 const final = [imports, typeImports, sourceImports, ...proxyFileOutput];
493 return final.join('\n') + '\n';
494}
495const GENERATED_DTS = 'components.d.ts';
496const IMPORT_TYPES = 'Components';
497
498const angularOutputTarget = (outputTarget) => ({
499 type: 'custom',
500 name: 'angular-library',
501 validate(config) {
502 return normalizeOutputTarget(config, outputTarget);
503 },
504 async generator(config, compilerCtx, buildCtx) {
505 const timespan = buildCtx.createTimeSpan(`generate angular proxies started`, true);
506 await angularDirectiveProxyOutput(compilerCtx, outputTarget, buildCtx.components, config);
507 timespan.finish(`generate angular proxies finished`);
508 },
509});
510function normalizeOutputTarget(config, outputTarget) {
511 const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfigs: outputTarget.valueAccessorConfigs || [] });
512 if (config.rootDir == null) {
513 throw new Error('rootDir is not set and it should be set by stencil itself');
514 }
515 if (outputTarget.directivesProxyFile == null) {
516 throw new Error('directivesProxyFile is required. Please set it in the Stencil config.');
517 }
518 if (outputTarget.directivesProxyFile && !path__default['default'].isAbsolute(outputTarget.directivesProxyFile)) {
519 results.directivesProxyFile = normalizePath(path__default['default'].join(config.rootDir, outputTarget.directivesProxyFile));
520 }
521 if (outputTarget.directivesArrayFile && !path__default['default'].isAbsolute(outputTarget.directivesArrayFile)) {
522 results.directivesArrayFile = normalizePath(path__default['default'].join(config.rootDir, outputTarget.directivesArrayFile));
523 }
524 if (outputTarget.includeSingleComponentAngularModules !== undefined) {
525 console.warn('**Experimental**: includeSingleComponentAngularModules is a developer preview feature and may change or be removed in the future.');
526 }
527 return results;
528}
529
530exports.angularOutputTarget = angularOutputTarget;