@storm-software/config-tools 1.21.5 → 1.22.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 1.22.0 (2024-01-26)
2
+
3
+
4
+ ### 🚀 Features
5
+
6
+ - **workspace-tools:** Added custom `npm-publish` executor ([3e6292dd](https://github.com/storm-software/storm-ops/commit/3e6292dd))
7
+
8
+
9
+ ### ❤️ Thank You
10
+
11
+ - Patrick Sullivan
12
+
1
13
 
2
14
 
3
15
 
package/jest.config.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { getJestConfig } from "@storm-software/testing-tools";
2
+
3
+ export default getJestConfig("packages/config-tools", true, "config-tools");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storm-software/config-tools",
3
- "version": "1.21.5",
3
+ "version": "1.22.0",
4
4
  "private": false,
5
5
  "description": "⚡The Storm-Ops monorepo contains utility applications, tools, and various libraries to create modern and scalable web applications.",
6
6
  "repository": {
package/project.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "config-tools",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "projectType": "library",
5
+ "sourceRoot": "packages/config-tools/src",
6
+ "targets": {
7
+ "build": {
8
+ "executor": "@nx/esbuild:esbuild",
9
+ "outputs": ["{options.outputPath}"],
10
+ "options": {
11
+ "main": "packages/config-tools/src/index.ts",
12
+ "additionalEntryPoints": [
13
+ "packages/config-tools/src/utilities/find-workspace-root.ts",
14
+ "packages/config-tools/src/utilities/logger.ts"
15
+ ],
16
+ "outputPath": "dist/packages/config-tools",
17
+ "tsConfig": "packages/config-tools/tsconfig.json",
18
+ "project": "packages/config-tools/package.json",
19
+ "defaultConfiguration": "production",
20
+ "platform": "node",
21
+ "deleteOutputPath": true,
22
+ "bundle": true,
23
+ "thirdParty": true,
24
+ "skipTypeCheck": true,
25
+ "metafile": true,
26
+ "minify": true,
27
+ "format": ["cjs", "esm"],
28
+ "assets": [
29
+ {
30
+ "input": "./packages/config-tools",
31
+ "glob": "*.md",
32
+ "output": "."
33
+ },
34
+ {
35
+ "input": "",
36
+ "glob": "LICENSE",
37
+ "output": "."
38
+ },
39
+ {
40
+ "input": "./packages/config-tools",
41
+ "glob": "declarations.d.ts",
42
+ "output": "."
43
+ }
44
+ ],
45
+ "configurations": {
46
+ "production": {
47
+ "debug": false,
48
+ "verbose": false
49
+ },
50
+ "development": {
51
+ "debug": true,
52
+ "verbose": true
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "lint": {},
58
+ "test": {}
59
+ }
60
+ }
@@ -0,0 +1,9 @@
1
+ import type { StormConfigInput } from "../types";
2
+
3
+ /**
4
+ * Type the config values for the current Storm workspace
5
+ *
6
+ * @param input - The config values for the current Storm workspace
7
+ * @returns The config values for the current Storm workspace
8
+ */
9
+ export const defineConfig = (input: StormConfigInput) => input;
@@ -0,0 +1,56 @@
1
+ import { type CosmiconfigResult, cosmiconfig } from "cosmiconfig";
2
+ import type { StormConfigInput } from "../types";
3
+
4
+ let _static_cache: StormConfigInput | undefined = undefined;
5
+
6
+ /**
7
+ * Get the config file for the current Storm workspace
8
+ *
9
+ * @param fileName - The name of the config file to search for
10
+ * @param filePath - The path to search for the config file in
11
+ * @returns The config file for the current Storm workspace
12
+ */
13
+ export const getConfigFileByName = async (
14
+ fileName: string,
15
+ filePath?: string
16
+ ): Promise<CosmiconfigResult> => cosmiconfig(fileName, { cache: true }).search(filePath);
17
+
18
+ /**
19
+ * Get the config file for the current Storm workspace
20
+ *
21
+ * @returns The config file for the current Storm workspace
22
+ */
23
+ export const getConfigFile = async (filePath?: string): Promise<StormConfigInput> => {
24
+ if (_static_cache) {
25
+ return _static_cache as StormConfigInput;
26
+ }
27
+
28
+ let cosmiconfigResult = await getConfigFileByName("storm", filePath);
29
+ if (!cosmiconfigResult || cosmiconfigResult.isEmpty) {
30
+ cosmiconfigResult = await getConfigFileByName("storm-software", filePath);
31
+ if (!cosmiconfigResult || cosmiconfigResult.isEmpty) {
32
+ cosmiconfigResult = await getConfigFileByName("storm-stack", filePath);
33
+ if (!cosmiconfigResult || cosmiconfigResult.isEmpty) {
34
+ cosmiconfigResult = await getConfigFileByName("storm-cloud", filePath);
35
+ }
36
+ }
37
+ }
38
+
39
+ if (
40
+ !cosmiconfigResult ||
41
+ Object.keys(cosmiconfigResult).length === 0 ||
42
+ cosmiconfigResult.isEmpty ||
43
+ !cosmiconfigResult.filepath
44
+ ) {
45
+ return undefined;
46
+ }
47
+
48
+ const config: Partial<StormConfigInput> = cosmiconfigResult.config ?? {};
49
+ if (cosmiconfigResult.filepath) {
50
+ config.configFile = cosmiconfigResult.filepath;
51
+ }
52
+ config.runtimeVersion = "0.0.1";
53
+
54
+ _static_cache = config;
55
+ return config;
56
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./get-config-file";
2
+ export * from "./define-config";
@@ -0,0 +1,129 @@
1
+ import type { ZodTypeAny } from "zod";
2
+ import { getConfigFile } from "./config-file/get-config-file";
3
+ import { getConfigEnv, getExtensionEnv } from "./env/get-env";
4
+ import { setConfigEnv } from "./env/set-env";
5
+ import { StormConfigSchema } from "./schema";
6
+ import type { StormConfig } from "./types";
7
+ import { findWorkspaceRoot, writeWarning } from "./utilities";
8
+ import { getDefaultConfig } from "./utilities/get-default-config";
9
+
10
+ const _extension_cache = new WeakMap<{ extensionName: string }, any>();
11
+ let _static_cache: StormConfig | undefined = undefined;
12
+
13
+ /**
14
+ * Get the config for the current Storm workspace
15
+ *
16
+ * @returns The config for the current Storm workspace
17
+ */
18
+ export const createConfig = (workspaceRoot?: string): StormConfig => {
19
+ return createStormConfig(undefined, undefined, workspaceRoot);
20
+ };
21
+
22
+ /**
23
+ * Get the config for the current Storm workspace
24
+ *
25
+ * @returns The config for the current Storm workspace
26
+ */
27
+ export const createStormConfig = <
28
+ TExtensionName extends keyof StormConfig["extensions"] = keyof StormConfig["extensions"],
29
+ TExtensionConfig = any,
30
+ TExtensionSchema extends ZodTypeAny = ZodTypeAny
31
+ >(
32
+ extensionName?: TExtensionName,
33
+ schema?: TExtensionSchema,
34
+ workspaceRoot?: string
35
+ ): StormConfig<TExtensionName, TExtensionConfig> => {
36
+ let result!: StormConfig<TExtensionName, TExtensionConfig>;
37
+ if (!_static_cache) {
38
+ const config = getConfigEnv() as StormConfig & {
39
+ [extensionName in TExtensionName]: TExtensionConfig;
40
+ };
41
+ const defaultConfig = getDefaultConfig(config, workspaceRoot);
42
+
43
+ result = StormConfigSchema.parse({
44
+ ...defaultConfig,
45
+ ...config,
46
+ colors: {
47
+ ...defaultConfig?.colors,
48
+ ...config.colors
49
+ }
50
+ }) as StormConfig<TExtensionName, TExtensionConfig>;
51
+ } else {
52
+ result = _static_cache as StormConfig<TExtensionName, TExtensionConfig>;
53
+ }
54
+
55
+ if (schema && extensionName) {
56
+ result.extensions = {
57
+ ...result.extensions,
58
+ [extensionName]: createConfigExtension<TExtensionName, TExtensionConfig, TExtensionSchema>(
59
+ extensionName,
60
+ schema
61
+ )
62
+ };
63
+ }
64
+
65
+ _static_cache = result;
66
+ return result;
67
+ };
68
+
69
+ /**
70
+ * Get the config for a specific Storm config Extension
71
+ *
72
+ * @param extensionName - The name of the config extension
73
+ * @param options - The options for the config extension
74
+ * @returns The config for the specified Storm config extension. If the extension does not exist, `undefined` is returned.
75
+ */
76
+ export const createConfigExtension = <
77
+ TExtensionName extends keyof StormConfig["extensions"] = keyof StormConfig["extensions"],
78
+ TExtensionConfig = any,
79
+ TExtensionSchema extends ZodTypeAny = ZodTypeAny
80
+ >(
81
+ extensionName: TExtensionName,
82
+ schema: TExtensionSchema
83
+ ): TExtensionConfig => {
84
+ const extension_cache_key = { extensionName };
85
+ if (_extension_cache.has(extension_cache_key)) {
86
+ return _extension_cache.get(extension_cache_key) as TExtensionConfig;
87
+ }
88
+
89
+ let extension = getExtensionEnv(extensionName);
90
+ if (schema) {
91
+ extension = schema.parse(extension);
92
+ }
93
+
94
+ _extension_cache.set(extension_cache_key, extension);
95
+ return extension as TExtensionConfig;
96
+ };
97
+
98
+ /**
99
+ * Load the config file values for the current Storm workspace into environment variables
100
+ */
101
+ export const loadStormConfig = async (workspaceRoot?: string): Promise<StormConfig> => {
102
+ let config = {} as StormConfig;
103
+
104
+ let _workspaceRoot = workspaceRoot;
105
+ if (!_workspaceRoot) {
106
+ _workspaceRoot = findWorkspaceRoot();
107
+ }
108
+
109
+ const configFile = await getConfigFile(_workspaceRoot);
110
+ if (!configFile) {
111
+ writeWarning(
112
+ { logLevel: "all" },
113
+ "No Storm config file found in the current workspace. Please ensure this is the expected behavior - you can add a `storm.config.js` file to the root of your workspace if it is not.\n"
114
+ );
115
+ }
116
+
117
+ config = StormConfigSchema.parse(
118
+ await getDefaultConfig(
119
+ {
120
+ ...configFile,
121
+ ...getConfigEnv()
122
+ },
123
+ _workspaceRoot
124
+ )
125
+ );
126
+ setConfigEnv(config);
127
+
128
+ return config;
129
+ };
@@ -0,0 +1,115 @@
1
+ import type { LogLevelLabel, StormConfig } from "../types";
2
+ import { getLogLevelLabel } from "../utilities";
3
+
4
+ /**
5
+ * Get the config for an extension module of Storm workspace from environment variables
6
+ *
7
+ * @param extensionName - The name of the extension module
8
+ * @returns The config for the specified Storm extension module. If the module does not exist, `undefined` is returned.
9
+ */
10
+ export const getExtensionEnv = <TConfig extends Record<string, any> = Record<string, any>>(
11
+ extensionName: string
12
+ ): TConfig | undefined => {
13
+ const prefix = `STORM_EXTENSION_${extensionName.toUpperCase()}_`;
14
+ return Object.keys(process.env)
15
+ .filter((key) => key.startsWith(prefix))
16
+ .reduce((ret: Record<string, any>, key: string) => {
17
+ const name = key
18
+ .replace(prefix, "")
19
+ .split("_")
20
+ .map((i) => (i.length > 0 ? i.trim().charAt(0).toUpperCase() + i.trim().slice(1) : ""))
21
+ .join("");
22
+ if (name) {
23
+ ret[name] = process.env[key];
24
+ }
25
+
26
+ return ret;
27
+ }, {}) as TConfig;
28
+ };
29
+
30
+ /**
31
+ * Get the config for the current Storm workspace
32
+ *
33
+ * @returns The config for the current Storm workspace from environment variables
34
+ */
35
+ export const getConfigEnv = (): Partial<StormConfig> => {
36
+ const prefix = "STORM_";
37
+
38
+ let config: Partial<StormConfig> = {
39
+ name: process.env[`${prefix}NAME`],
40
+ namespace: process.env[`${prefix}NAMESPACE`],
41
+ owner: process.env[`${prefix}OWNER`],
42
+ worker: process.env[`${prefix}WORKER`],
43
+ organization: process.env[`${prefix}ORGANIZATION`],
44
+ packageManager: process.env[`${prefix}PACKAGE_MANAGER`] as StormConfig["packageManager"],
45
+ license: process.env[`${prefix}LICENSE`],
46
+ homepage: process.env[`${prefix}HOMEPAGE`],
47
+ timezone: process.env[`${prefix}TIMEZONE`] ?? process.env.TZ,
48
+ locale: process.env[`${prefix}LOCALE`] ?? process.env.LOCALE,
49
+ configFile: process.env[`${prefix}CONFIG_FILE`],
50
+ workspaceRoot: process.env[`${prefix}WORKSPACE_ROOT`],
51
+ packageDirectory: process.env[`${prefix}PACKAGE_DIRECTORY`],
52
+ buildDirectory: process.env[`${prefix}BUILD_DIRECTORY`],
53
+ runtimeVersion: process.env[`${prefix}RUNTIME_VERSION`],
54
+ runtimeDirectory: process.env[`${prefix}RUNTIME_DIRECTORY`],
55
+ env: (process.env[`${prefix}ENV`] ??
56
+ process.env.NODE_ENV ??
57
+ process.env.ENVIRONMENT) as StormConfig["env"],
58
+ ci:
59
+ process.env[`${prefix}CI`] !== undefined
60
+ ? Boolean(
61
+ process.env[`${prefix}CI`] ?? process.env.CI ?? process.env.CONTINUOUS_INTEGRATION
62
+ )
63
+ : undefined,
64
+ colors: {
65
+ primary: process.env[`${prefix}COLOR_PRIMARY`],
66
+ background: process.env[`${prefix}COLOR_BACKGROUND`],
67
+ success: process.env[`${prefix}COLOR_SUCCESS`],
68
+ info: process.env[`${prefix}COLOR_INFO`],
69
+ warning: process.env[`${prefix}COLOR_WARNING`],
70
+ error: process.env[`${prefix}COLOR_ERROR`],
71
+ fatal: process.env[`${prefix}COLOR_FATAL`]
72
+ },
73
+ repository: process.env[`${prefix}REPOSITORY`],
74
+ branch: process.env[`${prefix}BRANCH`],
75
+ preMajor:
76
+ process.env[`${prefix}PRE_MAJOR`] !== undefined
77
+ ? Boolean(process.env[`${prefix}PRE_MAJOR`])
78
+ : undefined,
79
+ logLevel:
80
+ process.env[`${prefix}LOG_LEVEL`] !== null && process.env[`${prefix}LOG_LEVEL`] !== undefined
81
+ ? Number.isSafeInteger(Number.parseInt(process.env[`${prefix}LOG_LEVEL`]))
82
+ ? getLogLevelLabel(Number.parseInt(process.env[`${prefix}LOG_LEVEL`]))
83
+ : (process.env[`${prefix}LOG_LEVEL`] as LogLevelLabel)
84
+ : undefined
85
+ };
86
+
87
+ const serializedConfig = process.env[`${prefix}CONFIG`];
88
+ if (serializedConfig) {
89
+ const parsed = JSON.parse(serializedConfig);
90
+ config = {
91
+ ...config,
92
+ ...parsed,
93
+ colors: { ...config.colors, ...parsed.colors },
94
+ extensions: { ...config.extensions, ...parsed.extensions }
95
+ };
96
+ }
97
+
98
+ return config;
99
+
100
+ /*const extensionPrefix = `${prefix}EXTENSION_`;
101
+ return Object.keys(process.env)
102
+ .filter((key) => key.startsWith(extensionPrefix))
103
+ .reduce((ret: StormConfig, key: string) => {
104
+ const extensionName = key
105
+ .substring(prefix.length, key.indexOf("_", prefix.length))
106
+ .split("_")
107
+ .map((i) => (i.length > 0 ? i.trim().charAt(0).toUpperCase() + i.trim().slice(1) : ""))
108
+ .join("");
109
+ if (extensionName) {
110
+ ret.extensions[extensionName] = getExtensionEnv(extensionName);
111
+ }
112
+
113
+ return ret;
114
+ }, config);*/
115
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./get-env";
2
+ export * from "./set-env";
@@ -0,0 +1,162 @@
1
+ import { LogLevel, type StormConfig } from "../types";
2
+ import { getLogLevel } from "../utilities/get-log-level";
3
+
4
+ /**
5
+ * Get the config for an extension module of Storm workspace from environment variables
6
+ *
7
+ * @param extensionName - The name of the extension module
8
+ * @returns The config for the specified Storm extension module. If the module does not exist, `undefined` is returned.
9
+ */
10
+ export const setExtensionEnv = <TConfig extends Record<string, any> = Record<string, any>>(
11
+ extensionName: string,
12
+ extension: TConfig
13
+ ) => {
14
+ for (const key of Object.keys(extension ?? {})) {
15
+ if (extension[key]) {
16
+ const result =
17
+ key
18
+ ?.replace(/([A-Z])+/g, (input?: string) =>
19
+ input ? input[0].toUpperCase() + input.slice(1) : ""
20
+ )
21
+ .split(/(?=[A-Z])|[\.\-\s_]/)
22
+ .map((x: string) => x.toLowerCase()) ?? [];
23
+
24
+ let extensionKey: string;
25
+ if (result.length === 0) {
26
+ return;
27
+ }
28
+ if (result.length === 1) {
29
+ extensionKey = result[0].toUpperCase();
30
+ } else {
31
+ extensionKey = result.reduce((ret: string, part: string) => {
32
+ return `${ret}_${part.toLowerCase()}`;
33
+ });
34
+ }
35
+
36
+ process.env[`STORM_EXTENSION_${extensionName.toUpperCase()}_${extensionKey.toUpperCase()}`] =
37
+ extension[key];
38
+ }
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Get the config for the current Storm workspace
44
+ *
45
+ * @returns The config for the current Storm workspace from environment variables
46
+ */
47
+ export const setConfigEnv = (config: StormConfig) => {
48
+ const prefix = "STORM_";
49
+
50
+ if (config.name) {
51
+ process.env[`${prefix}NAME`] = config.name;
52
+ }
53
+ if (config.namespace) {
54
+ process.env[`${prefix}NAMESPACE`] = config.namespace;
55
+ }
56
+ if (config.owner) {
57
+ process.env[`${prefix}OWNER`] = config.owner;
58
+ }
59
+ if (config.worker) {
60
+ process.env[`${prefix}WORKER`] = config.worker;
61
+ }
62
+ if (config.organization) {
63
+ process.env[`${prefix}ORGANIZATION`] = config.organization;
64
+ }
65
+ if (config.packageManager) {
66
+ process.env[`${prefix}PACKAGE_MANAGER`] = config.packageManager;
67
+ }
68
+ if (config.license) {
69
+ process.env[`${prefix}LICENSE`] = config.license;
70
+ }
71
+ if (config.homepage) {
72
+ process.env[`${prefix}HOMEPAGE`] = config.homepage;
73
+ }
74
+ if (config.timezone) {
75
+ process.env[`${prefix}TIMEZONE`] = config.timezone;
76
+ process.env.TZ = config.timezone;
77
+ process.env.DEFAULT_TIMEZONE = config.timezone;
78
+ }
79
+ if (config.locale) {
80
+ process.env[`${prefix}LOCALE`] = config.locale;
81
+ process.env.LOCALE = config.locale;
82
+ process.env.DEFAULT_LOCALE = config.locale;
83
+ process.env.LANG = config.locale
84
+ ? `${config.locale.replaceAll("-", "_")}.UTF-8`
85
+ : "en_US.UTF-8";
86
+ }
87
+ if (config.configFile) {
88
+ process.env[`${prefix}CONFIG_FILE`] = config.configFile;
89
+ }
90
+ if (config.workspaceRoot) {
91
+ process.env[`${prefix}WORKSPACE_ROOT`] = config.workspaceRoot;
92
+ process.env.NX_WORKSPACE_ROOT = config.workspaceRoot;
93
+ process.env.NX_WORKSPACE_ROOT_PATH = config.workspaceRoot;
94
+ }
95
+ if (config.packageDirectory) {
96
+ process.env[`${prefix}PACKAGE_DIRECTORY`] = config.packageDirectory;
97
+ }
98
+ if (config.buildDirectory) {
99
+ process.env[`${prefix}BUILD_DIRECTORY`] = config.buildDirectory;
100
+ }
101
+ if (config.runtimeVersion) {
102
+ process.env[`${prefix}RUNTIME_VERSION`] = config.runtimeVersion;
103
+ }
104
+ if (config.runtimeDirectory) {
105
+ process.env[`${prefix}RUNTIME_DIRECTORY`] = config.runtimeDirectory;
106
+ }
107
+ if (config.env) {
108
+ process.env[`${prefix}ENV`] = config.env;
109
+ process.env.NODE_ENV = config.env;
110
+ process.env.ENVIRONMENT = config.env;
111
+ }
112
+ if (config.ci) {
113
+ process.env[`${prefix}CI`] = String(config.ci);
114
+ process.env.CI = String(config.ci);
115
+ process.env.CONTINUOUS_INTEGRATION = String(config.ci);
116
+ }
117
+ if (config.colors.primary) {
118
+ process.env[`${prefix}COLOR_PRIMARY`] = config.colors.primary;
119
+ }
120
+ if (config.colors.background) {
121
+ process.env[`${prefix}COLOR_BACKGROUND`] = config.colors.background;
122
+ }
123
+ if (config.colors.success) {
124
+ process.env[`${prefix}COLOR_SUCCESS`] = config.colors.success;
125
+ }
126
+ if (config.colors.info) {
127
+ process.env[`${prefix}COLOR_INFO`] = config.colors.info;
128
+ }
129
+ if (config.colors.warning) {
130
+ process.env[`${prefix}COLOR_WARNING`] = config.colors.warning;
131
+ }
132
+ if (config.colors.error) {
133
+ process.env[`${prefix}COLOR_ERROR`] = config.colors.error;
134
+ }
135
+ if (config.colors.fatal) {
136
+ process.env[`${prefix}COLOR_FATAL`] = config.colors.fatal;
137
+ }
138
+ if (config.repository) {
139
+ process.env[`${prefix}REPOSITORY`] = config.repository;
140
+ }
141
+ if (config.branch) {
142
+ process.env[`${prefix}BRANCH`] = config.branch;
143
+ }
144
+ if (config.preMajor) {
145
+ process.env[`${prefix}PRE_MAJOR`] = String(config.preMajor);
146
+ }
147
+ if (config.logLevel) {
148
+ process.env[`${prefix}LOG_LEVEL`] = String(config.logLevel);
149
+ process.env.LOG_LEVEL = String(config.logLevel);
150
+ process.env.NX_VERBOSE_LOGGING = String(
151
+ getLogLevel(config.logLevel) >= LogLevel.DEBUG ? true : false
152
+ );
153
+ process.env.RUST_BACKTRACE = getLogLevel(config.logLevel) >= LogLevel.DEBUG ? "full" : "none";
154
+ }
155
+ process.env[`${prefix}CONFIG`] = JSON.stringify(config);
156
+
157
+ for (const key of Object.keys(config.extensions ?? {})) {
158
+ config.extensions[key] &&
159
+ Object.keys(config.extensions[key]) &&
160
+ setExtensionEnv(key, config.extensions[key]);
161
+ }
162
+ };
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The config-tools library used by Storm Software for building TypeScript applications.
3
+ *
4
+ * @remarks
5
+ * A package containing various utilities to support custom workspace configurations
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ export * from "./config-file";
11
+ export * from "./create-storm-config";
12
+ export * from "./env";
13
+ export * from "./schema";
14
+ export * from "./types";
15
+ export * from "./utilities";