Skip to content

Commit

Permalink
feat(docs): add style mode to docs-json output (#5718)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rwaskiewicz committed May 3, 2024
1 parent 6723e30 commit 44fcba1
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 21 deletions.
2 changes: 1 addition & 1 deletion src/compiler/bundle/ext-transforms-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export const extTransformsPlugin = (
// Set style docs
if (cmp) {
cmp.styleDocs ||= [];
mergeIntoWith(cmp.styleDocs, cssTransformResults.styleDocs, (docs) => docs.name);
mergeIntoWith(cmp.styleDocs, cssTransformResults.styleDocs, (docs) => `${docs.name},${docs.mode}`);
}

// Track dependencies
Expand Down
31 changes: 21 additions & 10 deletions src/compiler/docs/generate-doc-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { flatOne, isOutputTargetDocsJson, join, normalizePath, relative, sortBy, unique } from '@utils';
import {
DEFAULT_STYLE_MODE,
flatOne,
isOutputTargetDocsJson,
join,
normalizePath,
relative,
sortBy,
unique,
} from '@utils';
import { basename, dirname } from 'path';

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

return sortBy(cmpMeta.styleDocs, (compilerStyleDoc) => compilerStyleDoc.name.toLowerCase()).map(
(compilerStyleDoc) => {
return {
name: compilerStyleDoc.name,
annotation: compilerStyleDoc.annotation || '',
docs: compilerStyleDoc.docs || '',
};
},
);
return sortBy(
cmpMeta.styleDocs,
(compilerStyleDoc) => `${compilerStyleDoc.name.toLowerCase()},${compilerStyleDoc.mode.toLowerCase()}}`,
).map((compilerStyleDoc) => {
return {
name: compilerStyleDoc.name,
annotation: compilerStyleDoc.annotation || '',
docs: compilerStyleDoc.docs || '',
mode: compilerStyleDoc.mode && compilerStyleDoc.mode !== DEFAULT_STYLE_MODE ? compilerStyleDoc.mode : undefined,
};
});
};

const getDocsListeners = (listeners: d.ComponentCompilerListener[]): d.JsonDocsListener[] => {
Expand Down
9 changes: 6 additions & 3 deletions src/compiler/docs/style-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import type * as d from '../../declarations';
*
* @param styleDocs the array to hold formatted CSS docstrings
* @param styleText the CSS text we're working with
* @param mode a mode associated with the parsed style, if applicable (e.g. this is not applicable for global styles)
*/
export function parseStyleDocs(styleDocs: d.StyleDoc[], styleText: string | null) {
export function parseStyleDocs(styleDocs: d.StyleDoc[], styleText: string | null, mode?: string | undefined) {
if (typeof styleText !== 'string') {
return;
}
Expand All @@ -27,7 +28,7 @@ export function parseStyleDocs(styleDocs: d.StyleDoc[], styleText: string | null
}

const comment = styleText.substring(0, endIndex);
parseCssComment(styleDocs, comment);
parseCssComment(styleDocs, comment, mode);

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

if (!styleDocs.some((c) => c.name === cssDoc.name && c.annotation === 'prop')) {
Expand Down
85 changes: 82 additions & 3 deletions src/compiler/docs/test/generate-doc-data.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mockBuildCtx, mockCompilerCtx, mockModule, mockValidatedConfig } from '@stencil/core/testing';
import { getComponentsFromModules } from '@utils';
import { DEFAULT_STYLE_MODE, getComponentsFromModules } from '@utils';

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

Expand All @@ -235,16 +236,19 @@ auto-generated content
annotation: 'prop',
docs: 'these are the docs for my-style-a',
name: 'my-style-a',
mode: 'ios',
};
const compilerStyleDocTwo: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are more docs for my-style-b',
name: 'my-style-b',
mode: 'ios',
};
const compilerStyleDocThree: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are more docs for my-style-c',
name: 'my-style-c',
mode: 'ios',
};
const compilerMeta = stubComponentCompilerMeta({
styleDocs: [compilerStyleDocOne, compilerStyleDocThree, compilerStyleDocTwo],
Expand All @@ -254,23 +258,48 @@ auto-generated content

expect(actual).toEqual([compilerStyleDocOne, compilerStyleDocTwo, compilerStyleDocThree]);
});

it('returns a sorted array from based on mode for the same name', () => {
const mdCompilerStyle: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are the docs for my-style-a',
name: 'my-style-a',
mode: 'md',
};
const iosCompilerStyle: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are the docs for my-style-a',
name: 'my-style-a',
mode: 'ios',
};
const compilerMeta = stubComponentCompilerMeta({
styleDocs: [mdCompilerStyle, iosCompilerStyle],
});

const actual = getDocsStyles(compilerMeta);

expect(actual).toEqual([iosCompilerStyle, mdCompilerStyle]);
});
});

it("returns CompilerStyleDoc with the same name in the order they're provided", () => {
const compilerStyleDocOne: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are the docs for my-style-a (first lowercase)',
name: 'my-style-a',
mode: 'ios',
};
const compilerStyleDocTwo: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are more docs for my-style-A (only capital)',
name: 'my-style-A',
mode: 'ios',
};
const compilerStyleDocThree: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are more docs for my-style-a (second lowercase)',
name: 'my-style-a',
mode: 'ios',
};
const compilerMeta = stubComponentCompilerMeta({
styleDocs: [compilerStyleDocOne, compilerStyleDocThree, compilerStyleDocTwo],
Expand All @@ -283,12 +312,13 @@ auto-generated content

describe('default values', () => {
it.each(['', null, undefined])(
'defaults the annotation to an empty string if %s is provided',
"defaults the annotation to an empty string if '%s' is provided",
(annotationValue) => {
const compilerStyleDoc: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are the docs for this prop',
name: 'my-style-one',
mode: DEFAULT_STYLE_MODE,
};
// @ts-ignore the intent of this test to verify the fallback of this field if it's falsy
compilerStyleDoc.annotation = annotationValue;
Expand All @@ -307,11 +337,12 @@ auto-generated content
},
);

it.each(['', null, undefined])('defaults the docs to an empty string if %s is provided', (docsValue) => {
it.each(['', null, undefined])("defaults the docs to an empty string if '%s' is provided", (docsValue) => {
const compilerStyleDoc: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are the docs for this prop',
name: 'my-style-one',
mode: DEFAULT_STYLE_MODE,
};
// @ts-ignore the intent of this test to verify the fallback of this field if it's falsy
compilerStyleDoc.docs = docsValue;
Expand All @@ -328,5 +359,53 @@ auto-generated content
},
]);
});

it.each(['', undefined, null, DEFAULT_STYLE_MODE])(
"uses 'undefined' for the mode value when '%s' is provided",
(modeValue) => {
const compilerStyleDoc: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are the docs for this prop',
name: 'my-style-one',
// we intentionally set this to non-compliant types for the purpose of this test, hence the type assertion
mode: modeValue as unknown as string,
};

const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] });

const actual = getDocsStyles(compilerMeta);

expect(actual).toEqual([
{
annotation: 'prop',
docs: 'these are the docs for this prop',
name: 'my-style-one',
mode: undefined,
},
]);
},
);

it('uses the mode value, when a valid string is provided', () => {
const compilerStyleDoc: d.CompilerStyleDoc = {
annotation: 'prop',
docs: 'these are the docs for this prop',
name: 'my-style-one',
mode: 'valid-string',
};

const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] });

const actual = getDocsStyles(compilerMeta);

expect(actual).toEqual([
{
annotation: 'prop',
docs: 'these are the docs for this prop',
name: 'my-style-one',
mode: 'valid-string',
},
]);
});
});
});
16 changes: 16 additions & 0 deletions src/compiler/docs/test/style-docs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type * as d from '@stencil/core/declarations';
import { DEFAULT_STYLE_MODE } from '@utils';

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

Expand Down Expand Up @@ -166,4 +167,19 @@ describe('style-docs', () => {
{ name: `--max-width-loud`, docs: `Max width of the alert (loud)`, annotation: 'prop' },
]);
});

it.each(['ios', 'md', undefined, '', DEFAULT_STYLE_MODE])("attaches mode metadata for a style mode '%s'", (mode) => {
const styleText = `
/*!
* @prop --max-width: Max width of the alert
*/
body {
color: red;
}
`;

parseStyleDocs(styleDocs, styleText, mode);

expect(styleDocs).toEqual([{ name: `--max-width`, docs: `Max width of the alert`, annotation: 'prop', mode }]);
});
});
2 changes: 1 addition & 1 deletion src/compiler/style/css-to-esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const transformCssToEsmModule = (input: d.TransformCssToEsmInput): d.TransformCs
};

if (input.docs) {
parseStyleDocs(results.styleDocs, input.input);
parseStyleDocs(results.styleDocs, input.input, input.mode);
}

try {
Expand Down
35 changes: 35 additions & 0 deletions src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,10 +849,29 @@ export interface CompilerJsDocTagInfo {
text?: string;
}

/**
* The (internal) representation of a CSS block comment in a CSS, Sass, etc. file. This data structure is used during
* the initial compilation phases of Stencil, as a piece of {@link ComponentCompilerMeta}.
*/
export interface CompilerStyleDoc {
/**
* The name of the CSS property
*/
name: string;
/**
* The user-defined description of the CSS property
*/
docs: string;
/**
* The JSDoc-style annotation (e.g. `@prop`) that was used in the block comment to detect the comment.
* Used to inform Stencil where the start of a new property's description starts (and where the previous description
* ends).
*/
annotation: 'prop';
/**
* The Stencil style-mode that is associated with this property.
*/
mode: string;
}

export interface CompilerAssetDir {
Expand Down Expand Up @@ -1887,6 +1906,22 @@ export interface TransformCssToEsmInput {
file?: string;
tag?: string;
encapsulation?: string;
/**
* The mode under which the CSS will be applied.
*
* Corresponds to a key used when `@Component`'s `styleUrls` field is an object:
* ```ts
* @Component({
* tag: 'todo-list',
* styleUrls: {
* ios: 'todo-list.ios.scss',
* md: 'todo-list.md.scss',
* }
* })
* ```
* In the example above, two `TransformCssToEsmInput`s should be created, one for 'ios' and one for 'md' (this field
* is not shared by multiple fields, nor is it a composite of multiple modes).
*/
mode?: string;
commentOriginalSelector?: boolean;
sourceMap?: boolean;
Expand Down
34 changes: 34 additions & 0 deletions src/declarations/stencil-public-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,26 @@ export interface JsonDocsEvent {
detail: string;
}

/**
* Type describing a CSS Style, as described by a JSDoc-style comment
*/
export interface JsonDocsStyle {
/**
* The name of the style
*/
name: string;
/**
* The type/description associated with the style
*/
docs: string;
/**
* The annotation used in the JSDoc of the style (e.g. `@prop`)
*/
annotation: string;
/**
* The mode associated with the style
*/
mode: string | undefined;
}

export interface JsonDocsListener {
Expand Down Expand Up @@ -346,8 +362,26 @@ export interface JsonDocsPart {
docs: string;
}

/**
* Represents a parsed block comment in a CSS, Sass, etc. file for a custom property.
*/
export interface StyleDoc {
/**
* The name of the CSS property
*/
name: string;
/**
* The user-defined description of the CSS property
*/
docs: string;
/**
* The JSDoc-style annotation (e.g. `@prop`) that was used in the block comment to detect the comment.
* Used to inform Stencil where the start of a new property's description starts (and where the previous description
* ends).
*/
annotation: 'prop';
/**
* The Stencil style-mode that is associated with this property.
*/
mode: string | undefined;
}

0 comments on commit 44fcba1

Please sign in to comment.