@omicronenergy/oscd-shell 0.0.7 → 0.0.9

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.
@@ -0,0 +1,138 @@
1
+ import { cyrb64 } from '../foundation/cyrb64.js';
2
+ const pluginTags = new Map();
3
+ /*
4
+ * Generates a unique tag name for a plugin based on its source URI.
5
+ * This is used to ensure that each plugin has a unique identifier in the CustomElements registry.
6
+ * @param uri - The source URI of the plugin.
7
+ * @returns A unique tag name for the plugin.
8
+ */
9
+ function pluginTag(uri) {
10
+ if (!pluginTags.has(uri)) {
11
+ pluginTags.set(uri, `oscd-p${cyrb64(uri)}`);
12
+ }
13
+ return pluginTags.get(uri);
14
+ }
15
+ /**
16
+ * Generates a Web Component class that displays an error message when a plugin fails to load.
17
+ * This is used to provide feedback to the Distro developer when a plugin cannot be loaded due to an error.
18
+ * @param plugin - The plugin object that failed to load.
19
+ * @returns A Web Component class that displays the error message.
20
+ */
21
+ function generateErrorWcClass(plugin) {
22
+ const title = 'Error: Plugin failed to load.';
23
+ const details = `Plugin: ${JSON.stringify(plugin)}`;
24
+ const classString = `
25
+ return class extends HTMLElement {
26
+
27
+ connectedCallback() {
28
+ this.innerHTML = '<h1>${title}</h1><p>${details}</p><emphasis>Check your plugins.json</emphasis>';
29
+ }
30
+
31
+ async run() {
32
+ alert('${title}\\n\\n ${details}; \\n\\n Check your plugins.json');
33
+ }
34
+ }`;
35
+ // eslint-disable-next-line no-new-func
36
+ return new Function(classString)();
37
+ }
38
+ /**
39
+ * Checks if the given object is a valid Plugin.
40
+ * @param plugin - The object to check.
41
+ * @returns true if the object is a Plugin, false otherwise.
42
+ */
43
+ export function isPlugin(plugin) {
44
+ return (typeof plugin === 'object' &&
45
+ plugin !== null &&
46
+ 'tagName' in plugin &&
47
+ typeof plugin.tagName === 'string');
48
+ }
49
+ /**
50
+ * Checks if the given object is a SourcedPlugin.
51
+ * @param plugin - The object to check.
52
+ * @returns true if the object is a SourcedPlugin, false otherwise.
53
+ */
54
+ export function isSourcedPlugin(plugin) {
55
+ return (typeof plugin === 'object' &&
56
+ plugin !== null &&
57
+ 'src' in plugin &&
58
+ typeof plugin.src === 'string');
59
+ }
60
+ /**
61
+ * Validates a Plugin object, checking for required fields and types.
62
+ * If the plugin is invalid, it logs an error and returns undefined.
63
+ * @param plugin - The plugin object to validate.
64
+ * @returns The validated Plugin object or undefined if invalid.
65
+ */
66
+ export function validatePlugin(plugin) {
67
+ const missingFields = [];
68
+ if (!isPlugin(plugin)) {
69
+ missingFields.push('tagName');
70
+ }
71
+ const _plugin = plugin;
72
+ missingFields.push(...['name', 'icon'].filter(field => !_plugin[field] || typeof _plugin[field] !== 'string'));
73
+ if (typeof _plugin.requireDoc !== 'undefined' &&
74
+ typeof _plugin.requireDoc !== 'boolean') {
75
+ missingFields.push('requireDoc');
76
+ }
77
+ if (typeof _plugin.translations !== 'undefined' &&
78
+ (typeof _plugin.translations !== 'object' ||
79
+ Object.values(_plugin.translations).some(t => typeof t !== 'string'))) {
80
+ missingFields.push('translations');
81
+ }
82
+ if (missingFields.length > 0) {
83
+ // eslint-disable-next-line no-console
84
+ console.error(`[Invalid Plugin]\n${JSON.stringify(plugin, null, 2)}\nMissing/Invalid fields [${missingFields.join(',')}] - skipping.`);
85
+ return undefined;
86
+ }
87
+ return _plugin;
88
+ }
89
+ /**
90
+ * Goes through all the plugins in the PluginSet and loads any sourced plugins, replacing the src field with a tagName.
91
+ * If a plugin does not have a tagName, it will be generated based on its src.
92
+ * All plugins returned are validated for required fields.
93
+ * If a sourced plugin fails to load (bad src), it will be replaced with an Error Web Component.
94
+ * @param plugins - Array of plugins to convert.
95
+ * @returns Array of plugins with tagName included.
96
+ */
97
+ export function loadSourcedPlugins(plugins) {
98
+ return plugins
99
+ .map(plugin => {
100
+ if (isPlugin(plugin)) {
101
+ return validatePlugin(plugin);
102
+ }
103
+ if (!isSourcedPlugin(plugin)) {
104
+ // eslint-disable-next-line no-console
105
+ console.error(`[Invalid Plugin] Requires a tagName or src - skipping. ${JSON.stringify(plugin)}`);
106
+ return undefined;
107
+ }
108
+ const { src, ...rest } = plugin;
109
+ const hashedTagName = pluginTag(src);
110
+ const validatedPlugin = validatePlugin({
111
+ ...rest,
112
+ tagName: hashedTagName,
113
+ });
114
+ if (!validatedPlugin) {
115
+ return undefined;
116
+ }
117
+ if (customElements.get(hashedTagName)) {
118
+ return validatedPlugin;
119
+ }
120
+ const url = new URL(src, window.location.href).toString();
121
+ import(url)
122
+ .then(mod => {
123
+ // Because this is async, we need to check (again) if the element is already defined.
124
+ if (!customElements?.get(hashedTagName)) {
125
+ customElements.define(hashedTagName, mod.default);
126
+ }
127
+ })
128
+ .catch(err => {
129
+ // eslint-disable-next-line no-console
130
+ console.error(`[Invalid Plugin] Failed to load plugin ${plugin.name} from ${url}`, err);
131
+ const ErrWc = generateErrorWcClass(plugin);
132
+ customElements.define(hashedTagName, ErrWc);
133
+ });
134
+ return validatedPlugin;
135
+ })
136
+ .filter((plugin) => plugin !== undefined);
137
+ }
138
+ //# sourceMappingURL=plugin-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-utils.js","sourceRoot":"","sources":["../../utils/plugin-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { cyrb64 } from '../foundation/cyrb64.js';\nimport { PluginEntry, SourcedPluginEntry } from '../oscd-shell.js';\n\nconst pluginTags = new Map<string, string>();\n\n/*\n * Generates a unique tag name for a plugin based on its source URI.\n * This is used to ensure that each plugin has a unique identifier in the CustomElements registry.\n * @param uri - The source URI of the plugin.\n * @returns A unique tag name for the plugin.\n */\nfunction pluginTag(uri: string): string {\n if (!pluginTags.has(uri)) {\n pluginTags.set(uri, `oscd-p${cyrb64(uri)}`);\n }\n return pluginTags.get(uri)!;\n}\n\n/**\n * Generates a Web Component class that displays an error message when a plugin fails to load.\n * This is used to provide feedback to the Distro developer when a plugin cannot be loaded due to an error.\n * @param plugin - The plugin object that failed to load.\n * @returns A Web Component class that displays the error message.\n */\nfunction generateErrorWcClass(plugin: Partial<PluginEntry>) {\n const title = 'Error: Plugin failed to load.';\n const details = `Plugin: ${JSON.stringify(plugin)}`;\n const classString = `\n return class extends HTMLElement {\n\n connectedCallback() {\n this.innerHTML = '<h1>${title}</h1><p>${details}</p><emphasis>Check your plugins.json</emphasis>';\n }\n\n async run() {\n alert('${title}\\\\n\\\\n ${details}; \\\\n\\\\n Check your plugins.json');\n }\n }`;\n // eslint-disable-next-line no-new-func\n return new Function(classString)();\n}\n\n/**\n * Checks if the given object is a valid Plugin.\n * @param plugin - The object to check.\n * @returns true if the object is a Plugin, false otherwise.\n */\nexport function isPlugin(plugin: unknown): plugin is PluginEntry {\n return (\n typeof plugin === 'object' &&\n plugin !== null &&\n 'tagName' in plugin &&\n typeof plugin.tagName === 'string'\n );\n}\n\n/**\n * Checks if the given object is a SourcedPlugin.\n * @param plugin - The object to check.\n * @returns true if the object is a SourcedPlugin, false otherwise.\n */\nexport function isSourcedPlugin(plugin: unknown): plugin is SourcedPluginEntry {\n return (\n typeof plugin === 'object' &&\n plugin !== null &&\n 'src' in plugin &&\n typeof plugin.src === 'string'\n );\n}\n\n/**\n * Validates a Plugin object, checking for required fields and types.\n * If the plugin is invalid, it logs an error and returns undefined.\n * @param plugin - The plugin object to validate.\n * @returns The validated Plugin object or undefined if invalid.\n */\nexport function validatePlugin(plugin: unknown): PluginEntry | undefined {\n const missingFields = [];\n if (!isPlugin(plugin)) {\n missingFields.push('tagName');\n }\n\n const _plugin = plugin as PluginEntry;\n missingFields.push(\n ...(['name', 'icon'] as const).filter(\n field => !_plugin[field] || typeof _plugin[field] !== 'string',\n ),\n );\n if (\n typeof _plugin.requireDoc !== 'undefined' &&\n typeof _plugin.requireDoc !== 'boolean'\n ) {\n missingFields.push('requireDoc');\n }\n if (\n typeof _plugin.translations !== 'undefined' &&\n (typeof _plugin.translations !== 'object' ||\n Object.values(_plugin.translations).some(t => typeof t !== 'string'))\n ) {\n missingFields.push('translations');\n }\n\n if (missingFields.length > 0) {\n // eslint-disable-next-line no-console\n console.error(\n `[Invalid Plugin]\\n${JSON.stringify(plugin, null, 2)}\\nMissing/Invalid fields [${missingFields.join(',')}] - skipping.`,\n );\n return undefined;\n }\n return _plugin;\n}\n\n/**\n * Goes through all the plugins in the PluginSet and loads any sourced plugins, replacing the src field with a tagName.\n * If a plugin does not have a tagName, it will be generated based on its src.\n * All plugins returned are validated for required fields.\n * If a sourced plugin fails to load (bad src), it will be replaced with an Error Web Component.\n * @param plugins - Array of plugins to convert.\n * @returns Array of plugins with tagName included.\n */\nexport function loadSourcedPlugins(\n plugins: Partial<PluginEntry | SourcedPluginEntry>[],\n): PluginEntry[] {\n return plugins\n .map(plugin => {\n if (isPlugin(plugin)) {\n return validatePlugin(plugin);\n }\n if (!isSourcedPlugin(plugin)) {\n // eslint-disable-next-line no-console\n console.error(\n `[Invalid Plugin] Requires a tagName or src - skipping. ${JSON.stringify(plugin)}`,\n );\n return undefined;\n }\n const { src, ...rest } = plugin as SourcedPluginEntry;\n const hashedTagName = pluginTag(src);\n const validatedPlugin = validatePlugin({\n ...rest,\n tagName: hashedTagName,\n });\n\n if (!validatedPlugin) {\n return undefined;\n }\n\n if (customElements.get(hashedTagName)) {\n return validatedPlugin;\n }\n\n const url = new URL(src, window.location.href).toString();\n import(url)\n .then(mod => {\n // Because this is async, we need to check (again) if the element is already defined.\n if (!customElements?.get(hashedTagName)) {\n customElements.define(hashedTagName, mod.default);\n }\n })\n .catch(err => {\n // eslint-disable-next-line no-console\n console.error(\n `[Invalid Plugin] Failed to load plugin ${plugin.name} from ${url}`,\n err,\n );\n const ErrWc = generateErrorWcClass(plugin);\n customElements.define(hashedTagName, ErrWc);\n });\n return validatedPlugin;\n })\n .filter((plugin): plugin is PluginEntry => plugin !== undefined);\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,115 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { isPlugin, isSourcedPlugin, validatePlugin } from './plugin-utils.js';
3
+ describe('Plugin Utils', () => {
4
+ describe('isPlugin', () => {
5
+ it('should return true for a valid Plugin object', () => {
6
+ const plugin = {
7
+ tagName: 'test-plugin',
8
+ name: 'Test Plugin',
9
+ icon: 'test-icon',
10
+ requireDoc: false,
11
+ };
12
+ expect(plugin).satisfies(isPlugin);
13
+ });
14
+ it('should return false for an object without tagName', () => {
15
+ const plugin = {
16
+ name: 'Test Plugin',
17
+ icon: 'test-icon',
18
+ requireDoc: false,
19
+ };
20
+ expect(plugin).to.not.satisfy(isPlugin);
21
+ });
22
+ it('should return false for a SourcePlugin', () => {
23
+ const plugin = {
24
+ name: 'Test Plugin',
25
+ icon: 'test-icon',
26
+ src: 'data:text/javascript;charset=utf-8,import%20%7B%20default%20as%20TestPlugin%20%7D%20from%20"./test-plugin.js";',
27
+ };
28
+ expect(plugin).to.not.satisfy(isPlugin);
29
+ });
30
+ });
31
+ describe('isSourcePlugin', () => {
32
+ it('should return true for a valid SourcePlugin object', () => {
33
+ const plugin = {
34
+ name: 'Test Plugin',
35
+ src: 'data:text/javascript;charset=utf-8,import%20%7B%20default%20as%20TestPlugin%20%7D%20from%20"./test-plugin.js";',
36
+ icon: 'test-icon',
37
+ };
38
+ expect(plugin).satisfies(isSourcedPlugin);
39
+ });
40
+ it('should return false for an object without a src field', () => {
41
+ const plugin = {
42
+ name: 'Test Plugin',
43
+ icon: 'test-icon',
44
+ };
45
+ expect(plugin).not.to.satisfy(isSourcedPlugin);
46
+ });
47
+ it('should return false for an Plugin object (has tagName, but no src)', () => {
48
+ const plugin = {
49
+ name: 'Test Plugin',
50
+ icon: 'test-icon',
51
+ tagName: 'test-plugin',
52
+ };
53
+ expect(plugin).not.to.satisfy(isSourcedPlugin);
54
+ });
55
+ });
56
+ });
57
+ describe('validatePlugin', () => {
58
+ it('returns a Plugin object, for a valid plugin', async () => {
59
+ const plugin = {
60
+ name: 'Tagless, Sourceless, Hopeless Plugin',
61
+ icon: 'coronavirus',
62
+ tagName: 'test-tagless-plugin',
63
+ };
64
+ expect(plugin).satisfies(validatePlugin);
65
+ });
66
+ it('returns undefined, for a plugin definition missing its "tagName" field', async () => {
67
+ const plugin = {
68
+ icon: 'coronavirus',
69
+ name: 'Tagless, Sourceless, Hopeless Plugin',
70
+ };
71
+ expect(plugin).not.to.satisfy(validatePlugin);
72
+ });
73
+ it('returns undefined, for a plugin definition missing its "name" field', async () => {
74
+ const plugin = {
75
+ icon: 'coronavirus',
76
+ tagName: 'test-tagless-plugin',
77
+ };
78
+ expect(plugin).not.to.satisfy(validatePlugin);
79
+ });
80
+ it('returns undefined, for a plugin definition missing its "icon" field', async () => {
81
+ const plugin = {
82
+ name: 'Tagless, Sourceless, Hopeless Plugin',
83
+ tagName: 'test-tagless-plugin',
84
+ };
85
+ expect(plugin).not.to.satisfy(validatePlugin);
86
+ });
87
+ it('returns undefined, for a plugin definition with invalid requireDoc field', async () => {
88
+ const plugin = {
89
+ name: 'Tagless, Sourceless, Hopeless Plugin',
90
+ tagName: 'test-tagless-plugin',
91
+ requireDoc: 'not-a-boolean',
92
+ };
93
+ expect(plugin).not.to.satisfy(validatePlugin);
94
+ });
95
+ it('returns undefined, for a plugin definition with invalid translations field', async () => {
96
+ const plugin = {
97
+ name: 'Tagless, Sourceless, Hopeless Plugin',
98
+ tagName: 'test-tagless-plugin',
99
+ translations: 'ops',
100
+ };
101
+ expect(plugin).not.to.satisfy(validatePlugin);
102
+ });
103
+ it('returns undefined, for a plugin definition with invalid translations', async () => {
104
+ const plugin = {
105
+ name: 'Tagless, Sourceless, Hopeless Plugin',
106
+ tagName: 'test-tagless-plugin',
107
+ translations: {
108
+ en: 'Tagless, Sourceless, Hopeless Plugin',
109
+ fr: 123, // Invalid translation (not a string)
110
+ },
111
+ };
112
+ expect(plugin).not.to.satisfy(validatePlugin);
113
+ });
114
+ });
115
+ //# sourceMappingURL=plugin-utils.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-utils.spec.js","sourceRoot":"","sources":["../../utils/plugin-utils.spec.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing';\nimport { isPlugin, isSourcedPlugin, validatePlugin } from './plugin-utils.js';\n\ndescribe('Plugin Utils', () => {\n describe('isPlugin', () => {\n it('should return true for a valid Plugin object', () => {\n const plugin = {\n tagName: 'test-plugin',\n name: 'Test Plugin',\n icon: 'test-icon',\n requireDoc: false,\n };\n expect(plugin).satisfies(isPlugin);\n });\n\n it('should return false for an object without tagName', () => {\n const plugin = {\n name: 'Test Plugin',\n icon: 'test-icon',\n requireDoc: false,\n };\n expect(plugin).to.not.satisfy(isPlugin);\n });\n\n it('should return false for a SourcePlugin', () => {\n const plugin = {\n name: 'Test Plugin',\n icon: 'test-icon',\n src: 'data:text/javascript;charset=utf-8,import%20%7B%20default%20as%20TestPlugin%20%7D%20from%20\"./test-plugin.js\";',\n };\n expect(plugin).to.not.satisfy(isPlugin);\n });\n });\n\n describe('isSourcePlugin', () => {\n it('should return true for a valid SourcePlugin object', () => {\n const plugin = {\n name: 'Test Plugin',\n src: 'data:text/javascript;charset=utf-8,import%20%7B%20default%20as%20TestPlugin%20%7D%20from%20\"./test-plugin.js\";',\n icon: 'test-icon',\n };\n expect(plugin).satisfies(isSourcedPlugin);\n });\n\n it('should return false for an object without a src field', () => {\n const plugin = {\n name: 'Test Plugin',\n icon: 'test-icon',\n };\n expect(plugin).not.to.satisfy(isSourcedPlugin);\n });\n\n it('should return false for an Plugin object (has tagName, but no src)', () => {\n const plugin = {\n name: 'Test Plugin',\n icon: 'test-icon',\n tagName: 'test-plugin',\n };\n expect(plugin).not.to.satisfy(isSourcedPlugin);\n });\n });\n});\n\ndescribe('validatePlugin', () => {\n it('returns a Plugin object, for a valid plugin', async () => {\n const plugin = {\n name: 'Tagless, Sourceless, Hopeless Plugin',\n icon: 'coronavirus',\n tagName: 'test-tagless-plugin',\n };\n\n expect(plugin).satisfies(validatePlugin);\n });\n\n it('returns undefined, for a plugin definition missing its \"tagName\" field', async () => {\n const plugin = {\n icon: 'coronavirus',\n name: 'Tagless, Sourceless, Hopeless Plugin',\n };\n\n expect(plugin).not.to.satisfy(validatePlugin);\n });\n\n it('returns undefined, for a plugin definition missing its \"name\" field', async () => {\n const plugin = {\n icon: 'coronavirus',\n tagName: 'test-tagless-plugin',\n };\n\n expect(plugin).not.to.satisfy(validatePlugin);\n });\n\n it('returns undefined, for a plugin definition missing its \"icon\" field', async () => {\n const plugin = {\n name: 'Tagless, Sourceless, Hopeless Plugin',\n tagName: 'test-tagless-plugin',\n };\n\n expect(plugin).not.to.satisfy(validatePlugin);\n });\n\n it('returns undefined, for a plugin definition with invalid requireDoc field', async () => {\n const plugin = {\n name: 'Tagless, Sourceless, Hopeless Plugin',\n tagName: 'test-tagless-plugin',\n requireDoc: 'not-a-boolean',\n };\n\n expect(plugin).not.to.satisfy(validatePlugin);\n });\n\n it('returns undefined, for a plugin definition with invalid translations field', async () => {\n const plugin = {\n name: 'Tagless, Sourceless, Hopeless Plugin',\n tagName: 'test-tagless-plugin',\n translations: 'ops',\n };\n\n expect(plugin).not.to.satisfy(validatePlugin);\n });\n\n it('returns undefined, for a plugin definition with invalid translations', async () => {\n const plugin = {\n name: 'Tagless, Sourceless, Hopeless Plugin',\n tagName: 'test-tagless-plugin',\n translations: {\n en: 'Tagless, Sourceless, Hopeless Plugin',\n fr: 123, // Invalid translation (not a string)\n },\n };\n\n expect(plugin).not.to.satisfy(validatePlugin);\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omicronenergy/oscd-shell",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "The shell component of OpenSCD, where plugins are loaded and the application is initialized.",
5
5
  "displayName": "OpenSCD Shell",
6
6
  "repository": {
@@ -22,10 +22,7 @@
22
22
  "url": "https://github.com/OMICRONEnergyOSS/oscd-shell/issues"
23
23
  },
24
24
  "homepage": "https://github.com/OMICRONEnergyOSS/oscd-shell#readme",
25
- "packageManager": "npm@11.3.0",
26
25
  "type": "module",
27
- "browser": "./dist/foundation.js",
28
- "main": "./dist/foundation.js",
29
26
  "exports": {
30
27
  ".": "./dist/foundation.js",
31
28
  "./oscd-shell.js": "./dist/oscd-shell.js"
@@ -38,29 +35,32 @@
38
35
  "scripts": {
39
36
  "lint": "eslint --ext .ts,.html . --ignore-path .gitignore && prettier \"**/*.ts\" --check --ignore-path .gitignore",
40
37
  "format": "eslint --ext .ts,.html . --fix --ignore-path .gitignore && prettier \"**/*.ts\" --write --ignore-path .gitignore",
41
- "start": "npm run build && cp -r ./demo ./dist/ && concurrently -k -r \"tsc -b --watch --preserveWatchOutput\" \"wds\"",
42
- "start:bundle": "npm run bundle && concurrently -k -r \"rollup -c rollup.config.js --watch\" \"wds --open dist/demo/index.html --watch\"",
38
+ "build": "rimraf dist/* && cp -r ./demo ./dist/ && npm run extract && npm run localize && tsc -b",
39
+ "bundle": "rimraf dist && npm run localize && rollup -c rollup.config.js",
40
+ "extract": "lit-localize extract",
41
+ "localize": "lit-localize build",
42
+ "start": "npm run build && concurrently -k -r \"tsc -b --watch --preserveWatchOutput\" \"wds --node-resolve\"",
43
+ "start:bundle": "npm run bundle && concurrently -k -r \"rollup -c rollup.config.js --watch\" \"wds --watch\"",
43
44
  "test": "npm run build && playwright install && wtr --coverage",
44
45
  "test:unit": "npm run build && playwright install && wtr --coverage --group default",
45
- "test:visual": "npm run build && playwright install && wtr --group visual",
46
46
  "test:watch": "npm run build && concurrently -k -r \"tsc -b --watch --preserveWatchOutput\" \"wtr --watch --coverage\"",
47
47
  "test:unit:watch": "npm run build && concurrently -k -r \"tsc -b --watch --preserveWatchOutput\" \"wtr --watch --coverage --group default\"",
48
+ "test:visual": "npm run build && playwright install && wtr --group visual",
48
49
  "test:update": "npm run build && wtr --group visual --update-visual-baseline",
49
50
  "analyze": "cem analyze --litelement",
50
- "extract": "lit-localize extract",
51
- "localize": "lit-localize build",
52
- "build": "rimraf dist && npm run extract && npm run localize && tsc -b",
53
51
  "doc": "npm run analyze -- --exclude dist && typedoc --out dist/doc oscd-shell.ts foundation.ts",
54
- "bundle": "rimraf dist && npm run localize && rollup -c rollup.config.js",
55
52
  "deploy": "npm run bundle && npm run doc && gh-pages --dist 'dist'",
56
53
  "prepare": "husky"
57
54
  },
58
55
  "dependencies": {
59
56
  "@lit/localize": "^0.12.2",
60
- "@omicronenergy/oscd-api": "^0.1.0",
57
+ "@omicronenergy/oscd-api": "^0.1.3",
58
+ "@omicronenergy/oscd-background-editv1": "^0.0.6",
61
59
  "@omicronenergy/oscd-editor": "^1.5.0",
62
- "@omicronenergy/oscd-test-utils": "^0.0.7",
63
- "@omicronenergy/oscd-ui": "^0.0.4",
60
+ "@omicronenergy/oscd-menu-open": "^0.0.6",
61
+ "@omicronenergy/oscd-menu-save": "^0.0.1",
62
+ "@omicronenergy/oscd-test-utils": "^0.0.8",
63
+ "@omicronenergy/oscd-ui": "^0.0.6",
64
64
  "@open-wc/scoped-elements": "^3.0.5",
65
65
  "@webcomponents/scoped-custom-element-registry": "^0.0.10",
66
66
  "lit": "^3.3.0",
@@ -99,7 +99,7 @@
99
99
  "sinon": "^21.0.0",
100
100
  "tsdoc": "^0.0.4",
101
101
  "typedoc": "^0.28.4",
102
- "typescript": "^5.5.3"
102
+ "typescript": "5.5.3"
103
103
  },
104
104
  "overrides": {
105
105
  "playwright": {
@@ -123,7 +123,7 @@
123
123
  "plugins": [
124
124
  "@typescript-eslint",
125
125
  "eslint-plugin-tsdoc",
126
- "prettier/recommended"
126
+ "prettier"
127
127
  ],
128
128
  "rules": {
129
129
  "no-unused-vars": "off",
@@ -146,7 +146,8 @@
146
146
  {
147
147
  "devDependencies": [
148
148
  "**/*.test.ts",
149
- "**/*.spec.ts"
149
+ "**/*.spec.ts",
150
+ "./dist"
150
151
  ]
151
152
  }
152
153
  ],
@@ -169,7 +170,8 @@
169
170
  {
170
171
  "ignorePackages": true
171
172
  }
172
- ]
173
+ ],
174
+ "prettier/prettier": "error"
173
175
  },
174
176
  "overrides": [
175
177
  {