@rws-framework/client 2.22.1 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,10 +11,17 @@ module.exports = async function(content) {
11
11
  let processedContent = content;
12
12
  const filePath = this.resourcePath;
13
13
  const isDev = this._compiler.options.mode === 'development';
14
- let isIgnored = false;
15
- let isDebugged = false;
16
14
  // timingStart('decorator_extraction');
17
- const decoratorExtract = LoadersHelper.extractRWSViewArgs(processedContent);
15
+ const decoratorExtract = await LoadersHelper.extractRWSViewArgsAsync(
16
+ processedContent,
17
+ false,
18
+ filePath,
19
+ this.addDependency,
20
+ this.query?.rwsWorkspaceDir,
21
+ this.query?.appRootDir,
22
+ isDev,
23
+ this.query?.publicDir
24
+ );
18
25
  const decoratorData = decoratorExtract ? decoratorExtract.viewDecoratorData : null;
19
26
 
20
27
  const cachedCode = processedContent;
@@ -34,19 +41,11 @@ module.exports = async function(content) {
34
41
  return content;
35
42
  }
36
43
 
37
- let templateName = null;
38
- let stylesPath = null;
44
+ let isIgnored = false;
45
+ let isDebugged = false;
39
46
 
40
47
  if(decoratorData.decoratorArgs){
41
48
  const decoratorArgs = decoratorData.decoratorArgs
42
-
43
- if(decoratorArgs.template){
44
- templateName = decoratorData.decoratorArgs.template || null;
45
- }
46
-
47
- if(decoratorArgs.styles){
48
- stylesPath = decoratorData.decoratorArgs.styles || null;
49
- }
50
49
 
51
50
  if(decoratorArgs.ignorePackaging){
52
51
  isIgnored = true;
@@ -64,19 +63,13 @@ module.exports = async function(content) {
64
63
 
65
64
  try {
66
65
  if(tagName){
67
- const [template, htmlFastImports, templateExists] = await LoadersHelper.getTemplate(filePath, this.addDependency, className, templateName, isDev);
68
-
69
- const styles = await LoadersHelper.getStyles(filePath, this.query?.rwsWorkspaceDir, this.query?.appRootDir,this.addDependency, templateExists, stylesPath, isDev, this.query?.publicDir);
70
-
71
66
  if(className){
72
- const replacedViewDecoratorContent = decoratorExtract.replacedDecorator;
67
+ const replacedViewDecoratorContent = decoratorExtract.replacedDecorator;
73
68
 
74
69
  if(replacedViewDecoratorContent){
75
- processedContent = `${template}\n${styles}\n${replacedViewDecoratorContent}`;
70
+ processedContent = replacedViewDecoratorContent;
76
71
  }
77
72
  }
78
-
79
- processedContent = `${htmlFastImports ? htmlFastImports + '\n' : ''}${processedContent}`;
80
73
  }
81
74
 
82
75
  const debugTsPath = filePath.replace('.ts','.debug.ts');
@@ -125,7 +125,115 @@ function _extractRWSViewDefs(fastOptions = {}, decoratorArgs = {})
125
125
  return [addedParamDefs, addedParams];
126
126
  }
127
127
 
128
- function extractRWSViewArgs(content, noReplace = false) {
128
+ function extractRWSViewArgs(content, noReplace = false, filePath = null, addDependency = null, rwsWorkspaceDir = null, appRootDir = null, isDev = false, publicDir = null) {
129
+ // If this is being called with only basic parameters (backward compatibility)
130
+ if (filePath === null || addDependency === null) {
131
+ return extractRWSViewArgsSync(content, noReplace);
132
+ }
133
+
134
+ // Otherwise, call the async version
135
+ return extractRWSViewArgsAsync(content, noReplace, filePath, addDependency, rwsWorkspaceDir, appRootDir, isDev, publicDir);
136
+ }
137
+
138
+ function extractRWSViewArgsSync(content, noReplace = false) {
139
+ const viewReg = /@RWSView\(\s*["']([^"']+)["'](?:\s*,\s*([\s\S]*?))?\s*\)\s*(.*?\s+)?class\s+([a-zA-Z0-9_-]+)\s+extends\s+RWSViewComponent/gm;
140
+
141
+ let m;
142
+ let tagName = null;
143
+ let className = null;
144
+ let classNamePrefix = null;
145
+ let decoratorArgs = null;
146
+
147
+ const _defaultRWSLoaderOptions = {
148
+ templatePath: 'template.html',
149
+ stylesPath: 'styles.scss',
150
+ fastOptions: { shadowOptions: { mode: 'open' } }
151
+ }
152
+
153
+ while ((m = viewReg.exec(content)) !== null) {
154
+ if (m.index === viewReg.lastIndex) {
155
+ viewReg.lastIndex++;
156
+ }
157
+
158
+ m.forEach((match, groupIndex) => {
159
+ if (groupIndex === 1) {
160
+ tagName = match;
161
+ }
162
+
163
+ if (groupIndex === 2) {
164
+ if (match) {
165
+ try {
166
+ decoratorArgs = JSON.parse(JSON.stringify(match));
167
+ } catch(e){
168
+ console.log(chalk.red('Decorator options parse error: ') + e.message + '\n Problematic line:');
169
+ console.log(`
170
+ @RWSView(${tagName}, ${match})
171
+ `);
172
+ console.log(chalk.yellowBright(`Decorator options failed to parse for "${tagName}" component.`) + ' { decoratorArgs } defaulting to null.');
173
+ console.log(match);
174
+
175
+ console.error(e);
176
+
177
+ throw new Error('Failed parsing @RWSView')
178
+ }
179
+ }
180
+ }
181
+
182
+ if (groupIndex === 3) {
183
+ if(match){
184
+ classNamePrefix = match;
185
+ }
186
+ }
187
+
188
+ if (groupIndex === 4) {
189
+ className = match;
190
+ }
191
+ });
192
+ }
193
+
194
+ if(!tagName){
195
+ return null;
196
+ }
197
+
198
+ let processedContent = content;
199
+ let fastOptions = _defaultRWSLoaderOptions.fastOptions;
200
+
201
+ if(decoratorArgs && decoratorArgs !== ''){
202
+ try {
203
+ decoratorArgs = json5.parse(decoratorArgs);
204
+ }catch(e){
205
+ // ignore parse errors for backward compatibility
206
+ }
207
+ }
208
+
209
+ if (decoratorArgs && decoratorArgs.fastElementOptions) {
210
+ fastOptions = decoratorArgs.fastElementOptions;
211
+ }
212
+
213
+ let replacedDecorator = null;
214
+
215
+ if(!noReplace){
216
+ const [addedParamDefs, addedParams] = _extractRWSViewDefs(fastOptions, decoratorArgs);
217
+ const replacedViewDecoratorContent = processedContent.replace(
218
+ viewReg,
219
+ `@RWSView('$1', null, { template: rwsTemplate, styles${addedParams.length ? ', options: {' + (addedParams.join(', ')) + '}' : ''} })\n$3class $4 extends RWSViewComponent `
220
+ );
221
+
222
+ replacedDecorator = `${addedParamDefs.join('\n')}\n${replacedViewDecoratorContent}`;
223
+ }
224
+
225
+ return {
226
+ viewDecoratorData: {
227
+ tagName,
228
+ className,
229
+ classNamePrefix,
230
+ decoratorArgs
231
+ },
232
+ replacedDecorator
233
+ }
234
+ }
235
+
236
+ async function extractRWSViewArgsAsync(content, noReplace = false, filePath = null, addDependency = null, rwsWorkspaceDir = null, appRootDir = null, isDev = false, publicDir = null) {
129
237
  const viewReg = /@RWSView\(\s*["']([^"']+)["'](?:\s*,\s*([\s\S]*?))?\s*\)\s*(.*?\s+)?class\s+([a-zA-Z0-9_-]+)\s+extends\s+RWSViewComponent/gm;
130
238
 
131
239
  let m;
@@ -206,16 +314,37 @@ function extractRWSViewArgs(content, noReplace = false) {
206
314
 
207
315
  let replacedDecorator = null;
208
316
 
209
- if(!noReplace){
210
- const [addedParamDefs, addedParams] = _extractRWSViewDefs(fastOptions, decoratorArgs);
211
- const replacedViewDecoratorContent = processedContent.replace(
317
+ if(!noReplace && filePath && addDependency){
318
+ const [addedParamDefs, addedParams] = _extractRWSViewDefs(fastOptions, decoratorArgs);
319
+
320
+ // Get template name and styles path from decorator args
321
+ let templateName = null;
322
+ let stylesPath = null;
323
+
324
+ if(decoratorArgs && decoratorArgs.template){
325
+ templateName = decoratorArgs.template;
326
+ }
327
+ if(decoratorArgs && decoratorArgs.styles){
328
+ stylesPath = decoratorArgs.styles;
329
+ }
330
+
331
+ // Generate template and styles
332
+ const [template, htmlFastImports, templateExists] = await getTemplate(filePath, addDependency, className, templateName, isDev);
333
+ const styles = await getStyles(filePath, rwsWorkspaceDir, appRootDir, addDependency, templateExists, stylesPath, isDev, publicDir);
334
+
335
+ // Extract original imports (everything before the @RWSView decorator)
336
+ const beforeDecorator = processedContent.substring(0, processedContent.search(/@RWSView/));
337
+ const afterDecoratorMatch = processedContent.match(/@RWSView[\s\S]*$/);
338
+ const afterDecorator = afterDecoratorMatch ? afterDecoratorMatch[0] : '';
339
+
340
+ const replacedViewDecoratorContent = afterDecorator.replace(
212
341
  viewReg,
213
- `@RWSView('$1', null, { template: rwsTemplate, styles${addedParams.length ? ', options: {' + (addedParams.join(', ')) + '}' : ''} })\n$3class $4 extends RWSViewComponent `
342
+ `${template}\n${styles}\n${addedParamDefs.join('\n')}\n@RWSView('$1', null, { template: rwsTemplate, styles${addedParams.length ? ', options: {' + (addedParams.join(', ')) + '}' : ''} })\n$3class $4 extends RWSViewComponent `
214
343
  );
215
344
 
216
345
  // console.log({replacedViewDecoratorContent});
217
346
 
218
- replacedDecorator = `${addedParamDefs.join('\n')}\n${replacedViewDecoratorContent}`;
347
+ replacedDecorator = `${htmlFastImports ? htmlFastImports + '\n' : ''}${beforeDecorator}${replacedViewDecoratorContent}`;
219
348
  }
220
349
 
221
350
  return {
@@ -290,4 +419,4 @@ let rwsTemplate: any = T.html<${className}>\`${templateContent}\`;
290
419
  return [template, htmlFastImports, templateExists];
291
420
  }
292
421
 
293
- module.exports = { getRWSLoaders, extractRWSViewArgs, getTemplate, getStyles }
422
+ module.exports = { getRWSLoaders, extractRWSViewArgs, extractRWSViewArgsAsync, getTemplate, getStyles }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rws-framework/client",
3
3
  "private": false,
4
- "version": "2.22.1",
4
+ "version": "2.23.0",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
7
7
  "docs": "typedoc --tsconfig ./tsconfig.json"
@@ -14,14 +14,13 @@ import { handleExternalChange } from './_attrs/_external_handler';
14
14
  import { IFastDefinition, isDefined, defineComponent, getDefinition } from './_definitions';
15
15
  import { on, $emitDown, observe, sendEventToOutside } from './_event_handling';
16
16
  import { domEvents } from '../events';
17
+ import CSSInjectionManager, { CSSInjectMode, ICSSInjectionOptions } from './_css_injection';
17
18
 
18
19
  type ComposeMethodType<
19
20
  T extends FoundationElementDefinition,
20
21
  K extends Constructable<RWSViewComponent>
21
22
  > = (this: K, elementDefinition: T) => (overrideDefinition?: OverrideFoundationElementDefinition<T>) => FoundationElementRegistry<FoundationElementDefinition, T>;
22
23
 
23
- type CSSInjectMode = 'adopted' | 'legacy' | 'both';
24
-
25
24
  const _DEFAULT_INJECT_CSS_CACHE_LIMIT_DAYS = 1;
26
25
 
27
26
  export interface IWithCompose<T extends RWSViewComponent> {
@@ -253,82 +252,39 @@ abstract class RWSViewComponent extends FoundationElement implements IRWSViewCom
253
252
  return RWSViewComponent.instances;
254
253
  }
255
254
 
256
- protected async injectStyles(styleLinks: string[], mode: CSSInjectMode = 'adopted', maxDaysExp?: number) {
257
- const dbName = 'css-cache';
258
- const storeName = 'styles';
259
- const db = await this.indexedDBService.openDB(dbName, storeName);
260
- const maxAgeMs = 1000 * 60 * 60 * 24; // 24h
261
- const maxDaysAge = maxDaysExp ? maxDaysExp : _DEFAULT_INJECT_CSS_CACHE_LIMIT_DAYS;
262
- const maxAgeDays = maxAgeMs * maxDaysAge;
263
-
264
- let adoptedSheets: CSSStyleSheet[] = [];
265
-
266
- let doneAdded = false;
267
-
268
- for (const styleLink of styleLinks) {
269
- const loadPromise = new Promise<void>(async (resolve, reject) => {
270
- if (mode === 'legacy' || mode === 'both') {
271
- const link = document.createElement('link');
272
- link.rel = 'stylesheet';
273
- link.href = styleLink;
274
- this.getShadowRoot().appendChild(link);
275
-
276
- link.onload = () => {
277
- doneAdded = true;
278
-
279
- if(mode === 'legacy'){
280
- resolve();
281
- }
282
- };
283
- }
284
-
285
- if (mode === 'adopted' || mode === 'both') {
286
- const entry = await this.indexedDBService.getFromDB(db, storeName, styleLink);
287
-
288
- let cssText: string | null = null;
289
-
290
- if (entry && typeof entry === 'object' && 'css' in entry && 'timestamp' in entry) {
291
- const expired = Date.now() - entry.timestamp > maxAgeDays;
292
- if (!expired) {
293
- cssText = entry.css;
294
- }
295
- }
296
-
297
- if (!cssText) {
298
- cssText = await fetch(styleLink).then(res => res.text());
299
- await this.indexedDBService.saveToDB(db, storeName, styleLink, {
300
- css: cssText,
301
- timestamp: Date.now()
302
- });
303
- console.log(`System saved stylesheet: ${styleLink} to IndexedDB`)
304
- }
305
-
306
- const sheet = new CSSStyleSheet();
307
- await sheet.replace(cssText);
255
+ static getCachedStyles(styleLinks: string[]): CSSStyleSheet[] {
256
+ return CSSInjectionManager.getCachedStyles(styleLinks);
257
+ }
308
258
 
309
- adoptedSheets.push(sheet);
259
+ static hasCachedStyles(styleLinks: string[]): boolean {
260
+ return CSSInjectionManager.hasCachedStyles(styleLinks);
261
+ }
310
262
 
311
- if(mode === 'adopted' || mode === 'both'){
312
- resolve();
313
- }
314
- }
315
- });
263
+ static getStylesOwnerComponent(): any {
264
+ return CSSInjectionManager.getStylesOwnerComponent();
265
+ }
316
266
 
317
- await loadPromise;
318
- }
267
+ static clearCachedStyles(): void {
268
+ CSSInjectionManager.clearCachedStyles();
269
+ }
319
270
 
320
- if (adoptedSheets.length) {
321
- this.getShadowRoot().adoptedStyleSheets = [
322
- ...adoptedSheets,
323
- ...this.getShadowRoot().adoptedStyleSheets,
324
- ];
271
+ protected async injectStyles(styleLinks: string[], mode: CSSInjectMode = 'adopted', maxDaysExp?: number) {
272
+ // Create a bridge object that exposes the necessary properties
273
+ const componentBridge = {
274
+ shadowRoot: this.shadowRoot,
275
+ indexedDBService: this.indexedDBService,
276
+ $emit: this.$emit.bind(this)
277
+ };
278
+
279
+ return CSSInjectionManager.injectStyles(componentBridge, styleLinks, { mode, maxDaysExp });
280
+ }
325
281
 
326
- doneAdded = true;
327
- }
282
+ protected getInjectedStyles(styleLinks: string[]): CSSStyleSheet[] {
283
+ return CSSInjectionManager.getCachedStyles(styleLinks);
284
+ }
328
285
 
329
- if (doneAdded) {
330
- this.$emit(domEvents.loadedLinkedStyles);
331
- }
286
+ protected hasInjectedStyles(styleLinks: string[]): boolean {
287
+ return CSSInjectionManager.hasCachedStyles(styleLinks);
332
288
  }
333
289
  }
334
290
 
@@ -0,0 +1,187 @@
1
+ import { domEvents } from '../events';
2
+ import IndexedDBService, { IndexedDBServiceInstance } from '../services/IndexedDBService';
3
+
4
+ type CSSInjectMode = 'adopted' | 'legacy' | 'both';
5
+
6
+ const _DEFAULT_INJECT_CSS_CACHE_LIMIT_DAYS = 1;
7
+
8
+ interface ICSSInjectionOptions {
9
+ mode?: CSSInjectMode;
10
+ maxDaysExp?: number;
11
+ }
12
+
13
+ interface ICSSInjectionComponent {
14
+ shadowRoot: ShadowRoot | null;
15
+ indexedDBService: IndexedDBServiceInstance;
16
+ $emit(eventName: string): void;
17
+ }
18
+
19
+ export class CSSInjectionManager {
20
+ private static CACHED_STYLES: Map<string, CSSStyleSheet> = new Map();
21
+ private static STYLES_OWNER_COMPONENT: ICSSInjectionComponent | null = null;
22
+
23
+ static getCachedStyles(styleLinks: string[]): CSSStyleSheet[] {
24
+ return styleLinks
25
+ .filter(link => CSSInjectionManager.CACHED_STYLES.has(link))
26
+ .map(link => CSSInjectionManager.CACHED_STYLES.get(link)!);
27
+ }
28
+
29
+ static hasCachedStyles(styleLinks: string[]): boolean {
30
+ return styleLinks.every(link => CSSInjectionManager.CACHED_STYLES.has(link));
31
+ }
32
+
33
+ static getStylesOwnerComponent(): ICSSInjectionComponent | null {
34
+ return CSSInjectionManager.STYLES_OWNER_COMPONENT;
35
+ }
36
+
37
+ static clearCachedStyles(): void {
38
+ CSSInjectionManager.CACHED_STYLES.clear();
39
+ CSSInjectionManager.STYLES_OWNER_COMPONENT = null;
40
+ }
41
+
42
+ static async injectStyles(
43
+ component: ICSSInjectionComponent,
44
+ styleLinks: string[],
45
+ options: ICSSInjectionOptions = {}
46
+ ): Promise<void> {
47
+ const { mode = 'adopted', maxDaysExp } = options;
48
+
49
+ if (!component.shadowRoot) {
50
+ throw new Error('Component must have a shadow root for CSS injection');
51
+ }
52
+
53
+ // Add initial transition styles to host element
54
+ const transitionSheet = new CSSStyleSheet();
55
+ await transitionSheet.replace(`
56
+ :host {
57
+ opacity: 0;
58
+ transition: opacity 0.3s ease-in-out;
59
+ }
60
+ `);
61
+ component.shadowRoot.adoptedStyleSheets = [
62
+ transitionSheet,
63
+ ...component.shadowRoot.adoptedStyleSheets,
64
+ ];
65
+
66
+ let adoptedSheets: CSSStyleSheet[] = [];
67
+ let doneAdded = false;
68
+
69
+ // Check if we already have cached styles from the owner component
70
+ const cachedSheets: CSSStyleSheet[] = [];
71
+ const uncachedLinks: string[] = [];
72
+
73
+ for (const styleLink of styleLinks) {
74
+ if (CSSInjectionManager.CACHED_STYLES.has(styleLink)) {
75
+ cachedSheets.push(CSSInjectionManager.CACHED_STYLES.get(styleLink)!);
76
+ } else {
77
+ uncachedLinks.push(styleLink);
78
+ }
79
+ }
80
+
81
+ // If we have cached styles, use them immediately
82
+ if (cachedSheets.length > 0) {
83
+ adoptedSheets.push(...cachedSheets);
84
+ doneAdded = true;
85
+ }
86
+
87
+ // Only process uncached styles
88
+ if (uncachedLinks.length > 0) {
89
+ // Set this component as the owner if no owner exists yet
90
+ if (!CSSInjectionManager.STYLES_OWNER_COMPONENT) {
91
+ CSSInjectionManager.STYLES_OWNER_COMPONENT = component;
92
+ }
93
+
94
+ const dbName = 'css-cache';
95
+ const storeName = 'styles';
96
+ const db = await component.indexedDBService.openDB(dbName, storeName);
97
+ const maxAgeMs = 1000 * 60 * 60 * 24; // 24h
98
+ const maxDaysAge = maxDaysExp ? maxDaysExp : _DEFAULT_INJECT_CSS_CACHE_LIMIT_DAYS;
99
+ const maxAgeDays = maxAgeMs * maxDaysAge;
100
+
101
+ for (const styleLink of uncachedLinks) {
102
+ const loadPromise = new Promise<void>(async (resolve, reject) => {
103
+ if (mode === 'legacy' || mode === 'both') {
104
+ const link = document.createElement('link');
105
+ link.rel = 'stylesheet';
106
+ link.href = styleLink;
107
+ component.shadowRoot!.appendChild(link);
108
+
109
+ link.onload = () => {
110
+ doneAdded = true;
111
+
112
+ if(mode === 'legacy'){
113
+ resolve();
114
+ }
115
+ };
116
+ }
117
+
118
+ if (mode === 'adopted' || mode === 'both') {
119
+ const entry = await component.indexedDBService.getFromDB(db, storeName, styleLink);
120
+
121
+ let cssText: string | null = null;
122
+
123
+ if (entry && typeof entry === 'object' && 'css' in entry && 'timestamp' in entry) {
124
+ const expired = Date.now() - entry.timestamp > maxAgeDays;
125
+ if (!expired) {
126
+ cssText = entry.css;
127
+ }
128
+ }
129
+
130
+ if (!cssText) {
131
+ cssText = await fetch(styleLink).then(res => res.text());
132
+ await component.indexedDBService.saveToDB(db, storeName, styleLink, {
133
+ css: cssText,
134
+ timestamp: Date.now()
135
+ });
136
+ console.log(`System saved stylesheet: ${styleLink} to IndexedDB`)
137
+ }
138
+
139
+ const sheet = new CSSStyleSheet();
140
+ await sheet.replace(cssText);
141
+
142
+ // Cache the stylesheet for future use
143
+ CSSInjectionManager.CACHED_STYLES.set(styleLink, sheet);
144
+
145
+ adoptedSheets.push(sheet);
146
+
147
+ if(mode === 'adopted' || mode === 'both'){
148
+ resolve();
149
+ }
150
+ }
151
+ });
152
+
153
+ await loadPromise;
154
+ }
155
+
156
+ doneAdded = true;
157
+ }
158
+
159
+ if (adoptedSheets.length) {
160
+ component.shadowRoot.adoptedStyleSheets = [
161
+ ...adoptedSheets,
162
+ ...component.shadowRoot.adoptedStyleSheets,
163
+ ];
164
+
165
+ doneAdded = true;
166
+ }
167
+
168
+ if (doneAdded) {
169
+ // Set opacity to 1 to fade in the component
170
+ const opacitySheet = new CSSStyleSheet();
171
+ await opacitySheet.replace(`
172
+ :host {
173
+ opacity: 1 !important;
174
+ }
175
+ `);
176
+ component.shadowRoot.adoptedStyleSheets = [
177
+ opacitySheet,
178
+ ...component.shadowRoot.adoptedStyleSheets,
179
+ ];
180
+
181
+ component.$emit(domEvents.loadedLinkedStyles);
182
+ }
183
+ }
184
+ }
185
+
186
+ export default CSSInjectionManager;
187
+ export { CSSInjectMode, ICSSInjectionOptions, ICSSInjectionComponent };