Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit a054f5e

Browse files
committed
Normalize paths shown in example tabs and for generated StackBlitz
This fixes issues where StackBlitz may show a directory named `.`. Can happen when an example component refers to a CSS file via `styleUrls` using `./my-css.css` compared to just `my-css.css`
1 parent 0ed6478 commit a054f5e

File tree

8 files changed

+886
-814
lines changed

8 files changed

+886
-814
lines changed

angular.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"builder": "@angular-devkit/build-angular:browser",
1818
"options": {
1919
"sourceMap": true,
20+
"allowedCommonJsDependencies": ["moment", "path-normalize"],
2021
"outputPath": "dist/material-angular-io",
2122
"index": "src/index.html",
2223
"polyfills": "src/polyfills.ts",

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,25 @@
3535
"private": true,
3636
"dependencies": {
3737
"@angular/animations": "^15.0.0-rc.1",
38-
"@angular/cdk": "^15.0.0-rc.0",
39-
"@angular/cdk-experimental": "^15.0.0-rc.0",
38+
"@angular/cdk": "^15.0.0",
39+
"@angular/cdk-experimental": "^15.0.0",
4040
"@angular/common": "^15.0.0-rc.1",
4141
"@angular/compiler": "^15.0.0-rc.1",
42-
"@angular/components-examples": "https://github.com/angular/material2-docs-content.git#5fbab42ef9cfab45a6e9b8ffc48ff91d57ce0337",
42+
"@angular/components-examples": "https://github.com/angular/material2-docs-content.git#5f06e33d3aab6a59b1f588cbdf5425dbfdf71663",
4343
"@angular/core": "^15.0.0-rc.1",
4444
"@angular/forms": "^15.0.0-rc.1",
4545
"@angular/google-maps": "^15.0.0-rc.0",
4646
"@angular/localize": "^15.0.0-rc.1",
47-
"@angular/material": "^15.0.0-rc.0",
48-
"@angular/material-experimental": "^15.0.0-rc.0",
49-
"@angular/material-moment-adapter": "^15.0.0-rc.0",
47+
"@angular/material": "^15.0.0",
48+
"@angular/material-experimental": "^15.0.0",
49+
"@angular/material-moment-adapter": "^15.0.0",
5050
"@angular/platform-browser": "^15.0.0-rc.1",
5151
"@angular/platform-browser-dynamic": "^15.0.0-rc.1",
5252
"@angular/router": "^15.0.0-rc.1",
53-
"@angular/youtube-player": "^15.0.0-rc.0",
53+
"@angular/youtube-player": "^15.0.0",
5454
"@stackblitz/sdk": "^1.5.2",
5555
"moment": "^2.29.1",
56+
"path-normalize": "^6.0.7",
5657
"rxjs": "^6.6.7",
5758
"tslib": "^2.3.0",
5859
"zone.js": "~0.11.5"

src/app/shared/example-viewer/example-viewer.spec.ts

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
2323
const exampleKey = 'autocomplete-overview';
2424
const exampleBasePath = `/docs-content/examples-highlighted/material/autocomplete/${exampleKey}`;
2525

26-
2726
describe('ExampleViewer', () => {
2827
let fixture: ComponentFixture<ExampleViewer>;
2928
let component: ExampleViewer;
@@ -32,12 +31,7 @@ describe('ExampleViewer', () => {
3231

3332
beforeEach(waitForAsync(() => {
3433
TestBed.configureTestingModule({
35-
imports: [
36-
DocViewerModule,
37-
DocsAppTestingModule,
38-
ReactiveFormsModule,
39-
TestExampleModule,
40-
],
34+
imports: [DocViewerModule, DocsAppTestingModule, ReactiveFormsModule, TestExampleModule],
4135
}).compileComponents();
4236
}));
4337

@@ -116,8 +110,7 @@ describe('ExampleViewer', () => {
116110
component.file = 'file.bad';
117111
component.selectCorrectTab();
118112

119-
expect(console.error).toHaveBeenCalledWith(
120-
`Could not find tab for file extension: "bad".`);
113+
expect(console.error).toHaveBeenCalledWith(`Could not find tab for file extension: "bad".`);
121114
}));
122115

123116
it('should set and return example properly', waitForAsync(() => {
@@ -149,7 +142,6 @@ describe('ExampleViewer', () => {
149142
}));
150143

151144
describe('view-source tab group', () => {
152-
153145
it('should only render HTML, TS and CSS files if no additional files are specified', () => {
154146
component.example = exampleKey;
155147

@@ -163,22 +155,26 @@ describe('ExampleViewer', () => {
163155
'additional-files-example.ts',
164156
'additional-files-example.html',
165157
'additional-files-example.css',
166-
'some-additional-file.html'
158+
'./some-file-using-a-dot.ts',
159+
'some-additional-file.html',
167160
],
168161
};
169162

170163
component.example = 'additional-files';
171164

172-
expect(component._getExampleTabNames())
173-
.toEqual(['HTML', 'TS', 'CSS', 'some-additional-file.html']);
165+
expect(component._getExampleTabNames()).toEqual([
166+
'HTML',
167+
'TS',
168+
'CSS',
169+
'some-file-using-a-dot.ts',
170+
'some-additional-file.html',
171+
]);
174172
});
175173

176174
it('should be possible for example to not have CSS or HTML files', () => {
177175
EXAMPLE_COMPONENTS['additional-files'] = {
178176
...EXAMPLE_COMPONENTS[exampleKey],
179-
files: [
180-
'additional-files-example.ts',
181-
],
177+
files: ['additional-files-example.ts'],
182178
};
183179

184180
component.example = 'additional-files';
@@ -205,38 +201,37 @@ describe('ExampleViewer', () => {
205201
button = btnDe ? btnDe.nativeElement : null;
206202
});
207203

208-
it('should call clipboard service when clicked', (() => {
204+
it('should call clipboard service when clicked', () => {
209205
const clipboardService = TestBed.inject(Clipboard);
210206
const spy = spyOn(clipboardService, 'copy');
211207
expect(spy.calls.count()).toBe(0, 'before click');
212208
button.click();
213209
expect(spy.calls.count()).toBe(1, 'after click');
214210
expect(spy.calls.argsFor(0)[0]).toBe('my docs page', 'click content');
215-
}));
211+
});
216212

217-
it('should display a message when copy succeeds', (() => {
213+
it('should display a message when copy succeeds', () => {
218214
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
219215
const clipboardService = TestBed.inject(Clipboard);
220216
spyOn(snackBar, 'open');
221217
spyOn(clipboardService, 'copy').and.returnValue(true);
222218
button.click();
223219
expect(snackBar.open).toHaveBeenCalledWith('Code copied', '', {duration: 2500});
224-
}));
220+
});
225221

226-
it('should display an error when copy fails', (() => {
222+
it('should display an error when copy fails', () => {
227223
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
228224
const clipboardService = TestBed.inject(Clipboard);
229225
spyOn(snackBar, 'open');
230226
spyOn(clipboardService, 'copy').and.returnValue(false);
231227
button.click();
232-
expect(snackBar.open)
233-
.toHaveBeenCalledWith('Copy failed. Please try again!', '', {duration: 2500});
234-
}));
228+
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', {
229+
duration: 2500,
230+
});
231+
});
235232
});
236-
237233
});
238234

239-
240235
// Create a version of ExampleModule for testing with only one component so that we don't have
241236
// to compile all of the examples for these tests.
242237
@NgModule({
@@ -251,12 +246,11 @@ describe('ExampleViewer', () => {
251246
AutocompleteExamplesModule,
252247
],
253248
})
254-
class TestExampleModule { }
255-
249+
class TestExampleModule {}
256250

257251
const FAKE_DOCS: {[key: string]: string} = {
258252
[`${exampleBasePath}/autocomplete-overview-example-html.html`]: '<div>my docs page</div>',
259253
[`${exampleBasePath}/autocomplete-overview-example-ts.html`]: '<span>const a = 1;</span>',
260254
[`${exampleBasePath}/autocomplete-overview-example-css.html`]:
261-
'<pre>.class { color: black; }</pre>',
255+
'<pre>.class { color: black; }</pre>',
262256
};

src/app/shared/example-viewer/example-viewer.ts

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import {
88
QueryList,
99
Type,
1010
ViewChildren,
11-
ɵNgModuleFactory
11+
ɵNgModuleFactory,
1212
} from '@angular/core';
1313
import {MatSnackBar} from '@angular/material/snack-bar';
1414
import {Clipboard} from '@angular/cdk/clipboard';
1515

1616
import {EXAMPLE_COMPONENTS, LiveExample} from '@angular/components-examples';
1717
import {CodeSnippet} from './code-snippet';
18+
import {normalizePath} from '../normalize-path';
1819

1920
export type Views = 'snippet' | 'full' | 'demo';
2021

@@ -27,7 +28,7 @@ const preferredExampleFileOrder = ['HTML', 'TS', 'CSS'];
2728
@Component({
2829
selector: 'example-viewer',
2930
templateUrl: './example-viewer.html',
30-
styleUrls: ['./example-viewer.scss']
31+
styleUrls: ['./example-viewer.scss'],
3132
})
3233
export class ExampleViewer implements OnInit {
3334
@ViewChildren(CodeSnippet) readonly snippet!: QueryList<CodeSnippet>;
@@ -39,16 +40,16 @@ export class ExampleViewer implements OnInit {
3940
exampleTabs: {[tabName: string]: string} = {};
4041

4142
/** Data for the currently selected example. */
42-
exampleData: LiveExample|null = null;
43+
exampleData: LiveExample | null = null;
4344

4445
/** URL to fetch code snippet for snippet view. */
4546
fileUrl: string | undefined;
4647

4748
/** Component type for the current example. */
48-
_exampleComponentType: Type<any>|null = null;
49+
_exampleComponentType: Type<any> | null = null;
4950

5051
/** Module factory that declares the example component. */
51-
_exampleModuleFactory: NgModuleFactory<any>|null = null;
52+
_exampleModuleFactory: NgModuleFactory<any> | null = null;
5253

5354
/** View of the example component. */
5455
@Input() view: Views | undefined;
@@ -67,8 +68,9 @@ export class ExampleViewer implements OnInit {
6768
this._example = exampleName;
6869
this.exampleData = EXAMPLE_COMPONENTS[exampleName];
6970
this._generateExampleTabs();
70-
this._loadExampleComponent().catch((error) =>
71-
console.error(`Could not load example '${exampleName}': ${error}`));
71+
this._loadExampleComponent().catch(error =>
72+
console.error(`Could not load example '${exampleName}': ${error}`)
73+
);
7274
} else {
7375
console.error(`Could not find example: ${exampleName}`);
7476
}
@@ -84,7 +86,8 @@ export class ExampleViewer implements OnInit {
8486
constructor(
8587
private readonly snackbar: MatSnackBar,
8688
private readonly clipboard: Clipboard,
87-
private readonly elementRef: ElementRef<HTMLElement>) {}
89+
private readonly elementRef: ElementRef<HTMLElement>
90+
) {}
8891

8992
ngOnInit() {
9093
if (this.file) {
@@ -146,26 +149,29 @@ export class ExampleViewer implements OnInit {
146149
fileName = `${contentBeforeDot}-${contentAfterDot}.html`;
147150
}
148151

149-
return this.exampleData ?
150-
`/docs-content/examples-highlighted/${this.exampleData.packagePath}/${fileName}` : '';
152+
return this.exampleData
153+
? `/docs-content/examples-highlighted/${this.exampleData.packagePath}/${fileName}`
154+
: '';
151155
}
152156

153157
_getExampleTabNames() {
154-
return this.exampleTabs ? Object.keys(this.exampleTabs).sort((a, b) => {
155-
let indexA = preferredExampleFileOrder.indexOf(a);
156-
let indexB = preferredExampleFileOrder.indexOf(b);
157-
// Files which are not part of the preferred example file order should be
158-
// moved after all items with a preferred index.
159-
if (indexA === -1) {
160-
indexA = preferredExampleFileOrder.length;
161-
}
162-
163-
if (indexB === -1) {
164-
indexB = preferredExampleFileOrder.length;
165-
}
166-
167-
return (indexA - indexB) || 1;
168-
}) : [];
158+
return this.exampleTabs
159+
? Object.keys(this.exampleTabs).sort((a, b) => {
160+
let indexA = preferredExampleFileOrder.indexOf(a);
161+
let indexB = preferredExampleFileOrder.indexOf(b);
162+
// Files which are not part of the preferred example file order should be
163+
// moved after all items with a preferred index.
164+
if (indexA === -1) {
165+
indexA = preferredExampleFileOrder.length;
166+
}
167+
168+
if (indexB === -1) {
169+
indexB = preferredExampleFileOrder.length;
170+
}
171+
172+
return indexA - indexB || 1;
173+
})
174+
: [];
169175
}
170176

171177
_copyLink() {
@@ -191,7 +197,8 @@ export class ExampleViewer implements OnInit {
191197
// files. More details: https://webpack.js.org/api/module-methods/#magic-comments.
192198
const moduleExports: any = await import(
193199
/* webpackExclude: /\.map$/ */
194-
'@angular/components-examples/fesm2020/' + module.importSpecifier);
200+
'@angular/components-examples/fesm2020/' + module.importSpecifier
201+
);
195202
this._exampleComponentType = moduleExports[componentName];
196203
// The components examples package is built with Ivy. This means that no factory files are
197204
// generated. To retrieve the factory of the AOT compiled module, we simply pass the module
@@ -217,18 +224,25 @@ export class ExampleViewer implements OnInit {
217224
const exampleBaseFileName = `${this.example}-example`;
218225
const docsContentPath = `/docs-content/examples-highlighted/${this.exampleData.packagePath}`;
219226

227+
const tsPath = normalizePath(`${exampleBaseFileName}.ts`);
228+
const cssPath = normalizePath(`${exampleBaseFileName}.css`);
229+
const htmlPath = normalizePath(`${exampleBaseFileName}.html`);
220230

221-
for (const fileName of this.exampleData.files) {
231+
for (let fileName of this.exampleData.files) {
222232
// Since the additional files refer to the original file name, we need to transform
223233
// the file name to match the highlighted HTML file that displays the source.
224234
const fileSourceName = fileName.replace(fileExtensionRegex, '$1-$2.html');
225235
const importPath = `${docsContentPath}/${fileSourceName}`;
226236

227-
if (fileName === `${exampleBaseFileName}.ts`) {
237+
// Normalize the path to allow for more consistent displaying in the tabs,
238+
// and to make comparisons below more reliable.
239+
fileName = normalizePath(fileName);
240+
241+
if (fileName === tsPath) {
228242
this.exampleTabs['TS'] = importPath;
229-
} else if (fileName === `${exampleBaseFileName}.css`) {
243+
} else if (fileName === cssPath) {
230244
this.exampleTabs['CSS'] = importPath;
231-
} else if (fileName === `${exampleBaseFileName}.html`) {
245+
} else if (fileName === htmlPath) {
232246
this.exampleTabs['HTML'] = importPath;
233247
} else {
234248
this.exampleTabs[fileName] = importPath;

src/app/shared/normalize-path.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as normalize from 'path-normalize';
2+
3+
/**
4+
* Normalizes the given path by:
5+
* - Collapsing unnecessary segments (e.g. `a/./b`)
6+
* - Normalizing from backslashes to Posix forward slashes.
7+
* - Removing a leading `./` if present.
8+
*/
9+
export function normalizePath(input: string): string {
10+
input = normalize(input.replace(/\\/g, '/'));
11+
if (input.startsWith('./')) {
12+
input = input.substring(2);
13+
}
14+
return input;
15+
}

0 commit comments

Comments
 (0)