1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | import * as assert from 'assert';
|
16 | import * as fs from 'fs';
|
17 | import * as jsonschema from 'jsonschema';
|
18 | import * as path from 'path';
|
19 | import * as logging from 'plylog';
|
20 | import {applyBuildPreset, isValidPreset, ProjectBuildOptions} from './builds';
|
21 | import minimatchAll = require('minimatch-all');
|
22 | import {FsUrlLoader, PackageUrlResolver, WarningFilter, Analyzer, Severity} from 'polymer-analyzer';
|
23 |
|
24 | export {ProjectBuildOptions, JsCompileTarget, applyBuildPreset} from './builds';
|
25 |
|
26 | const logger = logging.getLogger('polymer-project-config');
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | export const defaultSourceGlobs = ['src/**/*'];
|
32 |
|
33 | export type ModuleResolutionStrategy = 'none'|'node';
|
34 | const moduleResolutionStrategies =
|
35 | new Set<ModuleResolutionStrategy>(['none', 'node']);
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | function globResolve(fromPath: string, glob: string): string {
|
42 | if (glob.startsWith('!')) {
|
43 | const includeGlob = glob.substring(1);
|
44 | return '!' + path.resolve(fromPath, includeGlob);
|
45 | } else {
|
46 | return path.resolve(fromPath, glob);
|
47 | }
|
48 | }
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | function globRelative(fromPath: string, glob: string): string {
|
55 | if (glob.startsWith('!')) {
|
56 | return '!' + path.relative(fromPath, glob.substr(1));
|
57 | }
|
58 | return path.relative(fromPath, glob);
|
59 | }
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | function getPositiveGlob(glob: string): string {
|
65 | if (glob.startsWith('!')) {
|
66 | return glob.substring(1);
|
67 | } else {
|
68 | return glob;
|
69 | }
|
70 | }
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | function fixDeprecatedOptions(options: any): ProjectOptions {
|
78 | if (typeof options.sourceGlobs !== 'undefined') {
|
79 | logger.warn(
|
80 | '"sourceGlobs" config option has been renamed to "sources" and will no longer be supported in future versions');
|
81 | options.sources = options.sources || options.sourceGlobs;
|
82 | }
|
83 | if (typeof options.includeDependencies !== 'undefined') {
|
84 | logger.warn(
|
85 | '"includeDependencies" config option has been renamed to "extraDependencies" and will no longer be supported in future versions');
|
86 | options.extraDependencies =
|
87 | options.extraDependencies || options.includeDependencies;
|
88 | }
|
89 |
|
90 |
|
91 |
|
92 | if (options.lint && options.lint.warningsToIgnore) {
|
93 | options.lint.ignoreWarnings = options.lint.warningsToIgnore;
|
94 | } else if (options.lint && options.lint.ignoreWarnings) {
|
95 | options.lint.warningsToIgnore = options.lint.ignoreWarnings;
|
96 | }
|
97 | return options;
|
98 | }
|
99 |
|
100 | export interface LintOptions {
|
101 | |
102 |
|
103 |
|
104 |
|
105 | rules: string[];
|
106 |
|
107 | |
108 |
|
109 |
|
110 |
|
111 | warningsToIgnore?: string[];
|
112 |
|
113 | |
114 |
|
115 |
|
116 |
|
117 |
|
118 | ignoreWarnings?: string[];
|
119 |
|
120 | |
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | filesToIgnore?: string[];
|
131 | }
|
132 |
|
133 | export interface ProjectOptions {
|
134 | |
135 |
|
136 |
|
137 |
|
138 |
|
139 | root?: string;
|
140 |
|
141 | |
142 |
|
143 |
|
144 |
|
145 | entrypoint?: string;
|
146 |
|
147 | |
148 |
|
149 |
|
150 | shell?: string;
|
151 |
|
152 | |
153 |
|
154 |
|
155 |
|
156 | fragments?: string[];
|
157 |
|
158 | |
159 |
|
160 |
|
161 |
|
162 | sources?: string[];
|
163 |
|
164 | |
165 |
|
166 |
|
167 |
|
168 | extraDependencies?: string[];
|
169 |
|
170 | |
171 |
|
172 |
|
173 | builds?: ProjectBuildOptions[];
|
174 |
|
175 | |
176 |
|
177 |
|
178 | autoBasePath?: boolean;
|
179 |
|
180 | |
181 |
|
182 |
|
183 | lint?: LintOptions;
|
184 |
|
185 | |
186 |
|
187 |
|
188 |
|
189 |
|
190 | npm?: boolean;
|
191 |
|
192 | |
193 |
|
194 |
|
195 | componentDir?: string;
|
196 |
|
197 | |
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | moduleResolution?: ModuleResolutionStrategy;
|
205 | }
|
206 |
|
207 | export class ProjectConfig {
|
208 | readonly root: string;
|
209 | readonly entrypoint: string;
|
210 | readonly shell?: string;
|
211 | readonly fragments: string[];
|
212 | readonly sources: string[];
|
213 | readonly extraDependencies: string[];
|
214 | readonly componentDir?: string;
|
215 | readonly npm?: boolean;
|
216 | readonly moduleResolution: ModuleResolutionStrategy;
|
217 |
|
218 | readonly builds: ProjectBuildOptions[];
|
219 | readonly autoBasePath: boolean;
|
220 | readonly allFragments: string[];
|
221 | readonly lint: LintOptions|undefined = undefined;
|
222 |
|
223 | |
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 | static loadOptionsFromFile(filepath: string): ProjectOptions|null {
|
232 | try {
|
233 | const configContent = fs.readFileSync(filepath, 'utf-8');
|
234 | const contents = JSON.parse(configContent);
|
235 | return this.validateOptions(contents);
|
236 | } catch (error) {
|
237 |
|
238 | if (error && error.code === 'ENOENT') {
|
239 | logger.debug('no polymer config file found', {file: filepath});
|
240 | return null;
|
241 | }
|
242 |
|
243 | throw error;
|
244 | }
|
245 | }
|
246 |
|
247 | |
248 |
|
249 |
|
250 |
|
251 | static loadConfigFromFile(filepath: string): ProjectConfig|null {
|
252 | const configParsed = ProjectConfig.loadOptionsFromFile(filepath);
|
253 | if (!configParsed) {
|
254 | return null;
|
255 | }
|
256 | return new ProjectConfig(configParsed);
|
257 | }
|
258 |
|
259 | |
260 |
|
261 |
|
262 |
|
263 | static validateOptions(configJsonObject: {}): ProjectOptions {
|
264 | const validator = new jsonschema.Validator();
|
265 | const result = validator.validate(configJsonObject, getSchema());
|
266 | if (result.errors.length > 0) {
|
267 | const error = result.errors[0]!;
|
268 | if (!error.property && !error.message) {
|
269 | throw error;
|
270 | }
|
271 | let propertyName = error.property;
|
272 | if (propertyName.startsWith('instance.')) {
|
273 | propertyName = propertyName.slice(9);
|
274 | }
|
275 | throw new Error(`Property '${propertyName}' ${error.message}`);
|
276 | }
|
277 | return configJsonObject;
|
278 | }
|
279 |
|
280 | |
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 | static validateAndCreate(configJsonObject: {}) {
|
290 | const options = this.validateOptions(configJsonObject);
|
291 | return new this(options);
|
292 | }
|
293 |
|
294 | |
295 |
|
296 |
|
297 |
|
298 |
|
299 | static async initializeAnalyzerFromDirectory(dirname: string) {
|
300 | const config =
|
301 | this.loadConfigFromFile(path.join(dirname, 'polymer.json')) ||
|
302 | new ProjectConfig({root: dirname});
|
303 | return config.initializeAnalyzer();
|
304 | }
|
305 |
|
306 | |
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 | constructor(options: ProjectOptions) {
|
313 | options = (options) ? fixDeprecatedOptions(options) : {};
|
314 |
|
315 | |
316 |
|
317 |
|
318 | this.npm = options.npm;
|
319 |
|
320 |
|
321 | if (this.npm) {
|
322 | this.componentDir = 'node_modules/';
|
323 | }
|
324 |
|
325 | |
326 |
|
327 |
|
328 | this.moduleResolution = options.moduleResolution || 'node';
|
329 |
|
330 | |
331 |
|
332 |
|
333 | if (options.root) {
|
334 | this.root = path.resolve(options.root);
|
335 | } else {
|
336 | this.root = process.cwd();
|
337 | }
|
338 |
|
339 | |
340 |
|
341 |
|
342 | if (options.entrypoint) {
|
343 | this.entrypoint = path.resolve(this.root, options.entrypoint);
|
344 | } else {
|
345 | this.entrypoint = path.resolve(this.root, 'index.html');
|
346 | }
|
347 |
|
348 | |
349 |
|
350 |
|
351 | if (options.shell) {
|
352 | this.shell = path.resolve(this.root, options.shell);
|
353 | }
|
354 |
|
355 | |
356 |
|
357 |
|
358 | if (options.fragments) {
|
359 | this.fragments = options.fragments.map((e) => path.resolve(this.root, e));
|
360 | } else {
|
361 | this.fragments = [];
|
362 | }
|
363 |
|
364 | |
365 |
|
366 |
|
367 | this.extraDependencies = (options.extraDependencies ||
|
368 | []).map((glob) => globResolve(this.root, glob));
|
369 |
|
370 | |
371 |
|
372 |
|
373 | this.sources = (options.sources || defaultSourceGlobs)
|
374 | .map((glob) => globResolve(this.root, glob));
|
375 | this.sources.push(this.entrypoint);
|
376 | if (this.shell) {
|
377 | this.sources.push(this.shell);
|
378 | }
|
379 | if (this.fragments) {
|
380 | this.sources = this.sources.concat(this.fragments);
|
381 | }
|
382 |
|
383 | |
384 |
|
385 |
|
386 | this.allFragments = [];
|
387 |
|
388 | if (this.shell) {
|
389 | this.allFragments.push(this.shell);
|
390 | }
|
391 | if (this.fragments) {
|
392 | this.allFragments = this.allFragments.concat(this.fragments);
|
393 | }
|
394 | if (this.allFragments.length === 0) {
|
395 | this.allFragments.push(this.entrypoint);
|
396 | }
|
397 |
|
398 | if (options.lint) {
|
399 | this.lint = options.lint;
|
400 | }
|
401 |
|
402 | |
403 |
|
404 |
|
405 | if (options.builds) {
|
406 | this.builds = options.builds;
|
407 | if (Array.isArray(this.builds)) {
|
408 | this.builds = this.builds.map(applyBuildPreset);
|
409 | }
|
410 | }
|
411 |
|
412 | |
413 |
|
414 |
|
415 | if (options.autoBasePath) {
|
416 | this.autoBasePath = options.autoBasePath;
|
417 |
|
418 | for (const build of this.builds || []) {
|
419 | build.basePath = true;
|
420 | }
|
421 | }
|
422 |
|
423 | |
424 |
|
425 |
|
426 | if (options.componentDir) {
|
427 | this.componentDir = options.componentDir;
|
428 | }
|
429 | }
|
430 |
|
431 | |
432 |
|
433 |
|
434 |
|
435 | async initializeAnalyzer() {
|
436 | const urlLoader = new FsUrlLoader(this.root);
|
437 | const urlResolver = new PackageUrlResolver(
|
438 | {packageDir: this.root, componentDir: this.componentDir});
|
439 |
|
440 | const analyzer = new Analyzer({
|
441 | urlLoader,
|
442 | urlResolver,
|
443 | moduleResolution: convertModuleResolution(this.moduleResolution)
|
444 | });
|
445 | const lintConfig: Partial<LintOptions> = this.lint || {};
|
446 | const warningFilter = new WarningFilter({
|
447 | minimumSeverity: Severity.WARNING,
|
448 | warningCodesToIgnore: new Set(lintConfig.warningsToIgnore || []),
|
449 | filesToIgnore: lintConfig.filesToIgnore || []
|
450 | });
|
451 | return {urlLoader, urlResolver, analyzer, warningFilter};
|
452 | }
|
453 |
|
454 | isFragment(filepath: string): boolean {
|
455 | return this.allFragments.indexOf(filepath) !== -1;
|
456 | }
|
457 |
|
458 | isShell(filepath: string): boolean {
|
459 | return (!!this.shell && (this.shell === filepath));
|
460 | }
|
461 |
|
462 | isSource(filepath: string): boolean {
|
463 | return minimatchAll(filepath, this.sources);
|
464 | }
|
465 |
|
466 | |
467 |
|
468 |
|
469 |
|
470 | validate(): boolean {
|
471 | const validateErrorPrefix = `Polymer Config Error`;
|
472 | if (this.entrypoint) {
|
473 | assert(
|
474 | this.entrypoint.startsWith(this.root),
|
475 | `${validateErrorPrefix}: entrypoint (${this.entrypoint}) ` +
|
476 | `does not resolve within root (${this.root})`);
|
477 | }
|
478 | if (this.shell) {
|
479 | assert(
|
480 | this.shell.startsWith(this.root),
|
481 | `${validateErrorPrefix}: shell (${this.shell}) ` +
|
482 | `does not resolve within root (${this.root})`);
|
483 | }
|
484 | this.fragments.forEach((f) => {
|
485 | assert(
|
486 | f.startsWith(this.root),
|
487 | `${validateErrorPrefix}: a "fragments" path (${f}) ` +
|
488 | `does not resolve within root (${this.root})`);
|
489 | });
|
490 | this.sources.forEach((s) => {
|
491 | assert(
|
492 | getPositiveGlob(s).startsWith(this.root),
|
493 | `${validateErrorPrefix}: a "sources" path (${s}) ` +
|
494 | `does not resolve within root (${this.root})`);
|
495 | });
|
496 | this.extraDependencies.forEach((d) => {
|
497 | assert(
|
498 | getPositiveGlob(d).startsWith(this.root),
|
499 | `${validateErrorPrefix}: an "extraDependencies" path (${d}) ` +
|
500 | `does not resolve within root (${this.root})`);
|
501 | });
|
502 | assert(
|
503 | moduleResolutionStrategies.has(this.moduleResolution),
|
504 | `${validateErrorPrefix}: "moduleResolution" must be one of: ` +
|
505 | `${[...moduleResolutionStrategies].join(', ')}.`);
|
506 |
|
507 |
|
508 |
|
509 |
|
510 | if (this.builds) {
|
511 | assert(
|
512 | Array.isArray(this.builds),
|
513 | `${validateErrorPrefix}: "builds" (${this.builds}) ` +
|
514 | `expected an array of build configurations.`);
|
515 |
|
516 | if (this.builds.length > 1) {
|
517 | const buildNames = new Set<string>();
|
518 | for (const build of this.builds) {
|
519 | const buildName = build.name;
|
520 | const buildPreset = build.preset;
|
521 | assert(
|
522 | !buildPreset || isValidPreset(buildPreset),
|
523 | `${validateErrorPrefix}: "${buildPreset}" is not a valid ` +
|
524 | ` "builds" preset.`);
|
525 | assert(
|
526 | buildName,
|
527 | `${validateErrorPrefix}: all "builds" require ` +
|
528 | `a "name" property when there are multiple builds defined.`);
|
529 | assert(
|
530 | !buildNames.has(buildName!),
|
531 | `${validateErrorPrefix}: "builds" duplicate build name ` +
|
532 | `"${buildName}" found. Build names must be unique.`);
|
533 | buildNames.add(buildName!);
|
534 | }
|
535 | }
|
536 | }
|
537 |
|
538 | return true;
|
539 | }
|
540 |
|
541 | |
542 |
|
543 |
|
544 |
|
545 | toJSON(): string {
|
546 | let lintObj = undefined;
|
547 | if (this.lint) {
|
548 | lintObj = {...this.lint};
|
549 | delete lintObj.ignoreWarnings;
|
550 | }
|
551 | const isWindows = process.platform === 'win32';
|
552 | const normalizePath = isWindows ?
|
553 | (path: string) => path.replace(/\\/g, '/') :
|
554 | (path: string) => path;
|
555 | const obj = {
|
556 | entrypoint: globRelative(this.root, this.entrypoint),
|
557 | shell: this.shell ? globRelative(this.root, this.shell) : undefined,
|
558 | fragments: this.fragments.map((absolutePath) => {
|
559 | return normalizePath(globRelative(this.root, absolutePath));
|
560 | }),
|
561 | sources: this.sources.map((absolutePath) => {
|
562 | return normalizePath(globRelative(this.root, absolutePath));
|
563 | }),
|
564 | extraDependencies: this.extraDependencies.map((absolutePath) => {
|
565 | return normalizePath(globRelative(this.root, absolutePath));
|
566 | }),
|
567 | builds: this.builds,
|
568 | autoBasePath: this.autoBasePath,
|
569 | lint: lintObj,
|
570 | npm: this.npm,
|
571 | componentDir: this.componentDir,
|
572 | moduleResolution: this.moduleResolution,
|
573 | };
|
574 | return JSON.stringify(obj, null, 2);
|
575 | }
|
576 | }
|
577 |
|
578 |
|
579 |
|
580 |
|
581 | const getSchema: () => jsonschema.Schema = (() => {
|
582 | let schema: jsonschema.Schema;
|
583 |
|
584 | return () => {
|
585 | if (schema === undefined) {
|
586 | schema = JSON.parse(
|
587 | fs.readFileSync(path.join(__dirname, 'schema.json'), 'utf-8'));
|
588 | }
|
589 | return schema;
|
590 | };
|
591 | })();
|
592 |
|
593 |
|
594 | /**
|
595 | * Module resolution in ProjectConfig is different than the same-named parameter
|
596 | * in the analyzer. So we need to convert between the two.
|
597 | */
|
598 | function convertModuleResolution(moduleResolution: 'node'|'none'): 'node'|
|
599 | undefined {
|
600 | switch (moduleResolution) {
|
601 | case 'node':
|
602 | return 'node';
|
603 | case 'none':
|
604 | return undefined;
|
605 | default:
|
606 | const never: never = moduleResolution;
|
607 | throw new Error(`Unknown module resolution parameter: ${never}`);
|
608 | }
|
609 | }
|
610 |
|
\ | No newline at end of file |