Skip to content

Commit 44fcba1

Browse files
authored
feat(docs): add style mode to docs-json output (#5718)
add a style mode to the output of the `docs-json` output target. when a stencil component is declared using the `styleUrls` property, a user may choose to apply styles depending on a "mode" that they set at runtime: ```tsx // https://stenciljs.com/docs/component#styleurls import { Component } from '@stencil/core'; @component({ tag: 'todo-list', styleUrls: { ios: 'todo-list.ios.scss', md: 'todo-list.md.scss', } }) export class TodoList {} ``` where the `todo-list.ios.scss` stylesheet will be applied with the mode is set to 'ios' at runtime, and `todo-list.md.scss` will be applied if the mode 'md' is set at runtime. with this change, documented css properties will be associated with their respective modes in the output of the `docs-json` output target. for `todo-list.md.scss`: ```sass /** * @prop --button-background: Background of the button */ :host {} ``` the mode will now be reported in `docs-json`: ```diff { "name": "--button-background", "annotation": "prop", "docs": "Background of the button", + "mode": "md" }, ``` if a property of the same name exists in more than one mode - e.g. if `--button-background` _also_ existed in the ios mode stylesheet, two separate entries will be generated. this is accomplished by using a composite key for deduplicating/merging arrays consisting of the name and mode of the property/stylesheet STENCIL-1269 CSS Documentation Should Account for Modes
1 parent 6723e30 commit 44fcba1

14 files changed

+267
-21
lines changed

src/compiler/bundle/ext-transforms-plugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export const extTransformsPlugin = (
164164
// Set style docs
165165
if (cmp) {
166166
cmp.styleDocs ||= [];
167-
mergeIntoWith(cmp.styleDocs, cssTransformResults.styleDocs, (docs) => docs.name);
167+
mergeIntoWith(cmp.styleDocs, cssTransformResults.styleDocs, (docs) => `${docs.name},${docs.mode}`);
168168
}
169169

170170
// Track dependencies

src/compiler/docs/generate-doc-data.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { flatOne, isOutputTargetDocsJson, join, normalizePath, relative, sortBy, unique } from '@utils';
1+
import {
2+
DEFAULT_STYLE_MODE,
3+
flatOne,
4+
isOutputTargetDocsJson,
5+
join,
6+
normalizePath,
7+
relative,
8+
sortBy,
9+
unique,
10+
} from '@utils';
211
import { basename, dirname } from 'path';
312

413
import type * as d from '../../declarations';
@@ -316,15 +325,17 @@ export const getDocsStyles = (cmpMeta: d.ComponentCompilerMeta): d.JsonDocsStyle
316325
return [];
317326
}
318327

319-
return sortBy(cmpMeta.styleDocs, (compilerStyleDoc) => compilerStyleDoc.name.toLowerCase()).map(
320-
(compilerStyleDoc) => {
321-
return {
322-
name: compilerStyleDoc.name,
323-
annotation: compilerStyleDoc.annotation || '',
324-
docs: compilerStyleDoc.docs || '',
325-
};
326-
},
327-
);
328+
return sortBy(
329+
cmpMeta.styleDocs,
330+
(compilerStyleDoc) => `${compilerStyleDoc.name.toLowerCase()},${compilerStyleDoc.mode.toLowerCase()}}`,
331+
).map((compilerStyleDoc) => {
332+
return {
333+
name: compilerStyleDoc.name,
334+
annotation: compilerStyleDoc.annotation || '',
335+
docs: compilerStyleDoc.docs || '',
336+
mode: compilerStyleDoc.mode && compilerStyleDoc.mode !== DEFAULT_STYLE_MODE ? compilerStyleDoc.mode : undefined,
337+
};
338+
});
328339
};
329340

330341
const getDocsListeners = (listeners: d.ComponentCompilerListener[]): d.JsonDocsListener[] => {

src/compiler/docs/style-docs.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import type * as d from '../../declarations';
99
*
1010
* @param styleDocs the array to hold formatted CSS docstrings
1111
* @param styleText the CSS text we're working with
12+
* @param mode a mode associated with the parsed style, if applicable (e.g. this is not applicable for global styles)
1213
*/
13-
export function parseStyleDocs(styleDocs: d.StyleDoc[], styleText: string | null) {
14+
export function parseStyleDocs(styleDocs: d.StyleDoc[], styleText: string | null, mode?: string | undefined) {
1415
if (typeof styleText !== 'string') {
1516
return;
1617
}
@@ -27,7 +28,7 @@ export function parseStyleDocs(styleDocs: d.StyleDoc[], styleText: string | null
2728
}
2829

2930
const comment = styleText.substring(0, endIndex);
30-
parseCssComment(styleDocs, comment);
31+
parseCssComment(styleDocs, comment, mode);
3132

3233
styleText = styleText.substring(endIndex + CSS_DOC_END.length);
3334
match = styleText.match(CSS_DOC_START);
@@ -40,8 +41,9 @@ export function parseStyleDocs(styleDocs: d.StyleDoc[], styleText: string | null
4041
*
4142
* @param styleDocs an array which will be modified with the docstring
4243
* @param comment the comment string
44+
* @param mode a mode associated with the parsed style, if applicable (e.g. this is not applicable for global styles)
4345
*/
44-
function parseCssComment(styleDocs: d.StyleDoc[], comment: string): void {
46+
function parseCssComment(styleDocs: d.StyleDoc[], comment: string, mode: string | undefined): void {
4547
/**
4648
* @prop --max-width: Max width of the alert
4749
*/
@@ -77,6 +79,7 @@ function parseCssComment(styleDocs: d.StyleDoc[], comment: string): void {
7779
name: splt[0].trim(),
7880
docs: (splt.shift() && splt.join(`:`)).trim(),
7981
annotation: 'prop',
82+
mode,
8083
};
8184

8285
if (!styleDocs.some((c) => c.name === cssDoc.name && c.annotation === 'prop')) {

src/compiler/docs/test/generate-doc-data.spec.ts

+82-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mockBuildCtx, mockCompilerCtx, mockModule, mockValidatedConfig } from '@stencil/core/testing';
2-
import { getComponentsFromModules } from '@utils';
2+
import { DEFAULT_STYLE_MODE, getComponentsFromModules } from '@utils';
33

44
import type * as d from '../../../declarations';
55
import { stubComponentCompilerMeta } from '../../types/tests/ComponentCompilerMeta.stub';
@@ -222,6 +222,7 @@ auto-generated content
222222
annotation: 'prop',
223223
docs: 'these are the docs for this prop',
224224
name: 'my-style-one',
225+
mode: 'md',
225226
};
226227
const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] });
227228

@@ -235,16 +236,19 @@ auto-generated content
235236
annotation: 'prop',
236237
docs: 'these are the docs for my-style-a',
237238
name: 'my-style-a',
239+
mode: 'ios',
238240
};
239241
const compilerStyleDocTwo: d.CompilerStyleDoc = {
240242
annotation: 'prop',
241243
docs: 'these are more docs for my-style-b',
242244
name: 'my-style-b',
245+
mode: 'ios',
243246
};
244247
const compilerStyleDocThree: d.CompilerStyleDoc = {
245248
annotation: 'prop',
246249
docs: 'these are more docs for my-style-c',
247250
name: 'my-style-c',
251+
mode: 'ios',
248252
};
249253
const compilerMeta = stubComponentCompilerMeta({
250254
styleDocs: [compilerStyleDocOne, compilerStyleDocThree, compilerStyleDocTwo],
@@ -254,23 +258,48 @@ auto-generated content
254258

255259
expect(actual).toEqual([compilerStyleDocOne, compilerStyleDocTwo, compilerStyleDocThree]);
256260
});
261+
262+
it('returns a sorted array from based on mode for the same name', () => {
263+
const mdCompilerStyle: d.CompilerStyleDoc = {
264+
annotation: 'prop',
265+
docs: 'these are the docs for my-style-a',
266+
name: 'my-style-a',
267+
mode: 'md',
268+
};
269+
const iosCompilerStyle: d.CompilerStyleDoc = {
270+
annotation: 'prop',
271+
docs: 'these are the docs for my-style-a',
272+
name: 'my-style-a',
273+
mode: 'ios',
274+
};
275+
const compilerMeta = stubComponentCompilerMeta({
276+
styleDocs: [mdCompilerStyle, iosCompilerStyle],
277+
});
278+
279+
const actual = getDocsStyles(compilerMeta);
280+
281+
expect(actual).toEqual([iosCompilerStyle, mdCompilerStyle]);
282+
});
257283
});
258284

259285
it("returns CompilerStyleDoc with the same name in the order they're provided", () => {
260286
const compilerStyleDocOne: d.CompilerStyleDoc = {
261287
annotation: 'prop',
262288
docs: 'these are the docs for my-style-a (first lowercase)',
263289
name: 'my-style-a',
290+
mode: 'ios',
264291
};
265292
const compilerStyleDocTwo: d.CompilerStyleDoc = {
266293
annotation: 'prop',
267294
docs: 'these are more docs for my-style-A (only capital)',
268295
name: 'my-style-A',
296+
mode: 'ios',
269297
};
270298
const compilerStyleDocThree: d.CompilerStyleDoc = {
271299
annotation: 'prop',
272300
docs: 'these are more docs for my-style-a (second lowercase)',
273301
name: 'my-style-a',
302+
mode: 'ios',
274303
};
275304
const compilerMeta = stubComponentCompilerMeta({
276305
styleDocs: [compilerStyleDocOne, compilerStyleDocThree, compilerStyleDocTwo],
@@ -283,12 +312,13 @@ auto-generated content
283312

284313
describe('default values', () => {
285314
it.each(['', null, undefined])(
286-
'defaults the annotation to an empty string if %s is provided',
315+
"defaults the annotation to an empty string if '%s' is provided",
287316
(annotationValue) => {
288317
const compilerStyleDoc: d.CompilerStyleDoc = {
289318
annotation: 'prop',
290319
docs: 'these are the docs for this prop',
291320
name: 'my-style-one',
321+
mode: DEFAULT_STYLE_MODE,
292322
};
293323
// @ts-ignore the intent of this test to verify the fallback of this field if it's falsy
294324
compilerStyleDoc.annotation = annotationValue;
@@ -307,11 +337,12 @@ auto-generated content
307337
},
308338
);
309339

310-
it.each(['', null, undefined])('defaults the docs to an empty string if %s is provided', (docsValue) => {
340+
it.each(['', null, undefined])("defaults the docs to an empty string if '%s' is provided", (docsValue) => {
311341
const compilerStyleDoc: d.CompilerStyleDoc = {
312342
annotation: 'prop',
313343
docs: 'these are the docs for this prop',
314344
name: 'my-style-one',
345+
mode: DEFAULT_STYLE_MODE,
315346
};
316347
// @ts-ignore the intent of this test to verify the fallback of this field if it's falsy
317348
compilerStyleDoc.docs = docsValue;
@@ -328,5 +359,53 @@ auto-generated content
328359
},
329360
]);
330361
});
362+
363+
it.each(['', undefined, null, DEFAULT_STYLE_MODE])(
364+
"uses 'undefined' for the mode value when '%s' is provided",
365+
(modeValue) => {
366+
const compilerStyleDoc: d.CompilerStyleDoc = {
367+
annotation: 'prop',
368+
docs: 'these are the docs for this prop',
369+
name: 'my-style-one',
370+
// we intentionally set this to non-compliant types for the purpose of this test, hence the type assertion
371+
mode: modeValue as unknown as string,
372+
};
373+
374+
const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] });
375+
376+
const actual = getDocsStyles(compilerMeta);
377+
378+
expect(actual).toEqual([
379+
{
380+
annotation: 'prop',
381+
docs: 'these are the docs for this prop',
382+
name: 'my-style-one',
383+
mode: undefined,
384+
},
385+
]);
386+
},
387+
);
388+
389+
it('uses the mode value, when a valid string is provided', () => {
390+
const compilerStyleDoc: d.CompilerStyleDoc = {
391+
annotation: 'prop',
392+
docs: 'these are the docs for this prop',
393+
name: 'my-style-one',
394+
mode: 'valid-string',
395+
};
396+
397+
const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] });
398+
399+
const actual = getDocsStyles(compilerMeta);
400+
401+
expect(actual).toEqual([
402+
{
403+
annotation: 'prop',
404+
docs: 'these are the docs for this prop',
405+
name: 'my-style-one',
406+
mode: 'valid-string',
407+
},
408+
]);
409+
});
331410
});
332411
});

src/compiler/docs/test/style-docs.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type * as d from '@stencil/core/declarations';
2+
import { DEFAULT_STYLE_MODE } from '@utils';
23

34
import { parseStyleDocs } from '../style-docs';
45

@@ -166,4 +167,19 @@ describe('style-docs', () => {
166167
{ name: `--max-width-loud`, docs: `Max width of the alert (loud)`, annotation: 'prop' },
167168
]);
168169
});
170+
171+
it.each(['ios', 'md', undefined, '', DEFAULT_STYLE_MODE])("attaches mode metadata for a style mode '%s'", (mode) => {
172+
const styleText = `
173+
/*!
174+
* @prop --max-width: Max width of the alert
175+
*/
176+
body {
177+
color: red;
178+
}
179+
`;
180+
181+
parseStyleDocs(styleDocs, styleText, mode);
182+
183+
expect(styleDocs).toEqual([{ name: `--max-width`, docs: `Max width of the alert`, annotation: 'prop', mode }]);
184+
});
169185
});

src/compiler/style/css-to-esm.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const transformCssToEsmModule = (input: d.TransformCssToEsmInput): d.TransformCs
107107
};
108108

109109
if (input.docs) {
110-
parseStyleDocs(results.styleDocs, input.input);
110+
parseStyleDocs(results.styleDocs, input.input, input.mode);
111111
}
112112

113113
try {

src/declarations/stencil-private.ts

+35
Original file line numberDiff line numberDiff line change
@@ -849,10 +849,29 @@ export interface CompilerJsDocTagInfo {
849849
text?: string;
850850
}
851851

852+
/**
853+
* The (internal) representation of a CSS block comment in a CSS, Sass, etc. file. This data structure is used during
854+
* the initial compilation phases of Stencil, as a piece of {@link ComponentCompilerMeta}.
855+
*/
852856
export interface CompilerStyleDoc {
857+
/**
858+
* The name of the CSS property
859+
*/
853860
name: string;
861+
/**
862+
* The user-defined description of the CSS property
863+
*/
854864
docs: string;
865+
/**
866+
* The JSDoc-style annotation (e.g. `@prop`) that was used in the block comment to detect the comment.
867+
* Used to inform Stencil where the start of a new property's description starts (and where the previous description
868+
* ends).
869+
*/
855870
annotation: 'prop';
871+
/**
872+
* The Stencil style-mode that is associated with this property.
873+
*/
874+
mode: string;
856875
}
857876

858877
export interface CompilerAssetDir {
@@ -1887,6 +1906,22 @@ export interface TransformCssToEsmInput {
18871906
file?: string;
18881907
tag?: string;
18891908
encapsulation?: string;
1909+
/**
1910+
* The mode under which the CSS will be applied.
1911+
*
1912+
* Corresponds to a key used when `@Component`'s `styleUrls` field is an object:
1913+
* ```ts
1914+
* @Component({
1915+
* tag: 'todo-list',
1916+
* styleUrls: {
1917+
* ios: 'todo-list.ios.scss',
1918+
* md: 'todo-list.md.scss',
1919+
* }
1920+
* })
1921+
* ```
1922+
* In the example above, two `TransformCssToEsmInput`s should be created, one for 'ios' and one for 'md' (this field
1923+
* is not shared by multiple fields, nor is it a composite of multiple modes).
1924+
*/
18901925
mode?: string;
18911926
commentOriginalSelector?: boolean;
18921927
sourceMap?: boolean;

src/declarations/stencil-public-docs.ts

+34
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,26 @@ export interface JsonDocsEvent {
300300
detail: string;
301301
}
302302

303+
/**
304+
* Type describing a CSS Style, as described by a JSDoc-style comment
305+
*/
303306
export interface JsonDocsStyle {
307+
/**
308+
* The name of the style
309+
*/
304310
name: string;
311+
/**
312+
* The type/description associated with the style
313+
*/
305314
docs: string;
315+
/**
316+
* The annotation used in the JSDoc of the style (e.g. `@prop`)
317+
*/
306318
annotation: string;
319+
/**
320+
* The mode associated with the style
321+
*/
322+
mode: string | undefined;
307323
}
308324

309325
export interface JsonDocsListener {
@@ -346,8 +362,26 @@ export interface JsonDocsPart {
346362
docs: string;
347363
}
348364

365+
/**
366+
* Represents a parsed block comment in a CSS, Sass, etc. file for a custom property.
367+
*/
349368
export interface StyleDoc {
369+
/**
370+
* The name of the CSS property
371+
*/
350372
name: string;
373+
/**
374+
* The user-defined description of the CSS property
375+
*/
351376
docs: string;
377+
/**
378+
* The JSDoc-style annotation (e.g. `@prop`) that was used in the block comment to detect the comment.
379+
* Used to inform Stencil where the start of a new property's description starts (and where the previous description
380+
* ends).
381+
*/
352382
annotation: 'prop';
383+
/**
384+
* The Stencil style-mode that is associated with this property.
385+
*/
386+
mode: string | undefined;
353387
}

0 commit comments

Comments
 (0)