diff --git a/src/compiler/bundle/ext-transforms-plugin.ts b/src/compiler/bundle/ext-transforms-plugin.ts index 64354b3ea45..2293fac4fc6 100644 --- a/src/compiler/bundle/ext-transforms-plugin.ts +++ b/src/compiler/bundle/ext-transforms-plugin.ts @@ -3,9 +3,29 @@ import type { Plugin } from 'rollup'; import type * as d from '../../declarations'; import { runPluginTransformsEsmImports } from '../plugin/plugin'; +import { getScopeId } from '../style/scope-css'; import { parseImportPath } from '../transformers/stencil-import-path'; import type { BundleOptions } from './bundle-interface'; +/** + * This keeps a map of all the component styles we've seen already so we can create + * a correct state of all styles when we're doing a rebuild. This map helps by + * storing the state of all styles as follows, e.g.: + * + * ``` + * { + * 'cmp-a-$': { + * '/path/to/project/cmp-a.scss': 'button{color:red}', + * '/path/to/project/cmp-a.md.scss': 'button{color:blue}' + * } + * ``` + * + * Whenever one of the files change, we can propagate a correct concatenated + * version of all styles to the browser by setting `buildCtx.stylesUpdated`. + */ +type ComponentStyleMap = Map; +const allCmpStyles = new Map(); + /** * A Rollup plugin which bundles up some transformation of CSS imports as well * as writing some files to disk for the `DIST_COLLECTION` output target. @@ -42,6 +62,14 @@ export const extTransformsPlugin = ( return null; } + /** + * Make sure compiler context has a registered worker. The interface suggests that it + * potentially can be undefined, therefore check for it here. + */ + if (!compilerCtx.worker) { + return null; + } + // The `id` here was possibly previously updated using // `serializeImportPath` to annotate the filepath with various metadata // serialized to query-params. If that was done for this particular `id` @@ -49,6 +77,7 @@ export const extTransformsPlugin = ( const { data } = parseImportPath(id); if (data != null) { + let cmpStyles: ComponentStyleMap | undefined = undefined; let cmp: d.ComponentCompilerMeta | undefined = undefined; const filePath = normalizeFsPath(id); const code = await compilerCtx.fs.readFile(filePath); @@ -56,6 +85,13 @@ export const extTransformsPlugin = ( return null; } + /** + * add file to watch list if it is outside of the `srcDir` config path + */ + if (config.watch && (id.startsWith('/') || id.startsWith('.')) && !id.startsWith(config.srcDir)) { + compilerCtx.addWatchFile(id.split('?')[0]); + } + const pluginTransforms = await runPluginTransformsEsmImports(config, compilerCtx, buildCtx, code, filePath); // We need to check whether the current build is a dev-mode watch build w/ HMR enabled in @@ -94,6 +130,15 @@ export const extTransformsPlugin = ( }), ); } + + /** + * initiate map for component styles + */ + const scopeId = getScopeId(data.tag, data.mode); + if (!allCmpStyles.has(scopeId)) { + allCmpStyles.set(scopeId, new Map()); + } + cmpStyles = allCmpStyles.get(scopeId); } const cssTransformResults = await compilerCtx.worker.transformCssToEsm({ @@ -109,6 +154,13 @@ export const extTransformsPlugin = ( docs: config.buildDocs, }); + /** + * persist component styles for transformed stylesheet + */ + if (cmpStyles) { + cmpStyles.set(filePath, cssTransformResults.styleText); + } + // Set style docs if (cmp) { cmp.styleDocs = cssTransformResults.styleDocs; @@ -131,11 +183,40 @@ export const extTransformsPlugin = ( return s.styleTag === data.tag && s.styleMode === data.mode && s.styleText === cssTransformResults.styleText; }); - if (!hasUpdatedStyle) { + /** + * if the style has updated, compose all styles for the component + */ + if (!hasUpdatedStyle && data.tag && data.mode) { + const externalStyles = cmp?.styles?.[0]?.externalStyles; + + /** + * if component has external styles, use a list to keep the order to which + * styles are applied. + */ + const styleText = cmpStyles + ? externalStyles + ? /** + * attempt to find the original `filePath` key through `originalComponentPath` + * and `absolutePath` as path can differ based on how Stencil is installed + * e.g. through `npm link` or `npm install` + */ + externalStyles + .map((es) => cmpStyles.get(es.originalComponentPath) || cmpStyles.get(es.absolutePath)) + .join('\n') + : /** + * if `externalStyles` is not defined, then created the style text in the + * order of which the styles were compiled. + */ + [...cmpStyles.values()].join('\n') + : /** + * if `cmpStyles` is not defined, then use the style text from the transform + * as it is not connected to a component. + */ + cssTransformResults.styleText; buildCtx.stylesUpdated.push({ styleTag: data.tag, styleMode: data.mode, - styleText: cssTransformResults.styleText, + styleText, }); } diff --git a/src/compiler/plugin/plugin.ts b/src/compiler/plugin/plugin.ts index c5874f28452..e0e6b4dc836 100644 --- a/src/compiler/plugin/plugin.ts +++ b/src/compiler/plugin/plugin.ts @@ -57,6 +57,20 @@ export const runPluginLoad = async (pluginCtx: PluginCtx, id: string) => { return pluginCtx.fs.readFile(id); }; +/** + * returns a subset of the baseline array of strings + * @param baseline baseline of files + * @param superset files that were added by a transform + * @returns files that were added by a transform but haven't been part of the baseline + */ +const getDependencySubset = (baseline: string[] | undefined, superset: string[] = []) => { + if (!Array.isArray(baseline)) { + return []; + } + + return baseline.filter((f) => !superset.includes(f)); +}; + export const runPluginTransforms = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, @@ -137,6 +151,13 @@ export const runPluginTransforms = async ( if (isString(pluginTransformResults.id)) { transformResults.id = pluginTransformResults.id; } + + /** + * add dependencies from plugin transform results, e.g. transformed sass files + */ + transformResults.dependencies.push( + ...getDependencySubset(pluginTransformResults.dependencies, transformResults.dependencies), + ); } } } @@ -166,7 +187,9 @@ export const runPluginTransforms = async ( cmp.styleDocs, ); transformResults.code = cssParseResults.styleText; - transformResults.dependencies = cssParseResults.imports; + transformResults.dependencies.push( + ...getDependencySubset(cssParseResults.imports, transformResults.dependencies), + ); } else { const cssParseResults = await parseCssImports( config, @@ -177,7 +200,9 @@ export const runPluginTransforms = async ( transformResults.code, ); transformResults.code = cssParseResults.styleText; - transformResults.dependencies = cssParseResults.imports; + transformResults.dependencies.push( + ...getDependencySubset(cssParseResults.imports, transformResults.dependencies), + ); } }