@dxos/config 0.7.5-main.b19bfc8 → 0.7.5-main.d9d2d4e

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/config",
3
- "version": "0.7.5-main.b19bfc8",
3
+ "version": "0.7.5-main.d9d2d4e",
4
4
  "description": "Config utilities",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,6 +8,31 @@
8
8
  "author": "DXOS.org",
9
9
  "sideEffects": true,
10
10
  "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/types/src/index.d.ts",
14
+ "browser": "./dist/lib/browser/index.mjs",
15
+ "node": {
16
+ "require": "./dist/lib/node/index.cjs",
17
+ "default": "./dist/lib/node-esm/index.mjs"
18
+ }
19
+ },
20
+ "./esbuild-plugin": {
21
+ "types": "./dist/types/src/esbuild-plugin/index.d.ts",
22
+ "import": "./dist/plugin/node-esm/esbuild-plugin.mjs",
23
+ "require": "./dist/plugin/node/esbuild-plugin.cjs"
24
+ },
25
+ "./rollup-plugin": {
26
+ "types": "./dist/types/src/rollup-plugin/index.d.ts",
27
+ "import": "./dist/plugin/node-esm/rollup-plugin.mjs",
28
+ "require": "./dist/plugin/node/rollup-plugin.cjs"
29
+ },
30
+ "./vite-plugin": {
31
+ "types": "./dist/types/src/vite-plugin/index.d.ts",
32
+ "import": "./dist/plugin/node-esm/vite-plugin.mjs",
33
+ "require": "./dist/plugin/node/vite-plugin.cjs"
34
+ }
35
+ },
11
36
  "main": "dist/lib/node/index.cjs",
12
37
  "browser": {
13
38
  "./src/loaders/index.ts": "./src/loaders/browser.js",
@@ -16,16 +41,22 @@
16
41
  "./dist/lib/node/index.cjs": "./dist/lib/browser/index.mjs"
17
42
  },
18
43
  "types": "dist/types/src/index.d.ts",
44
+ "typesVersions": {
45
+ "*": {
46
+ "esbuild-plugin": [
47
+ "dist/types/src/esbuild-plugin/index.d.ts"
48
+ ],
49
+ "rollup-plugin": [
50
+ "dist/types/src/rollup-plugin/index.d.ts"
51
+ ],
52
+ "vite-plugin": [
53
+ "dist/types/src/vite-plugin/index.d.ts"
54
+ ]
55
+ }
56
+ },
19
57
  "files": [
20
58
  "dist",
21
- "esbuild-plugin.js",
22
- "esbuild-plugin.d.ts",
23
- "rollup-plugin.js",
24
- "rollup-plugin.d.ts",
25
- "vite-plugin.js",
26
- "vite-plugin.d.ts",
27
- "webpack-plugin.js",
28
- "webpack-plugin.d.ts"
59
+ "src"
29
60
  ],
30
61
  "dependencies": {
31
62
  "boolean": "^3.0.1",
@@ -34,11 +65,11 @@
34
65
  "lodash.defaultsdeep": "^4.6.1",
35
66
  "lodash.ismatch": "^4.4.0",
36
67
  "pkg-up": "^3.1.0",
37
- "@dxos/log": "0.7.5-main.b19bfc8",
38
- "@dxos/tracing": "0.7.5-main.b19bfc8",
39
- "@dxos/protocols": "0.7.5-main.b19bfc8",
40
- "@dxos/util": "0.7.5-main.b19bfc8",
41
- "@dxos/node-std": "0.7.5-main.b19bfc8"
68
+ "@dxos/log": "0.7.5-main.d9d2d4e",
69
+ "@dxos/node-std": "0.7.5-main.d9d2d4e",
70
+ "@dxos/protocols": "0.7.5-main.d9d2d4e",
71
+ "@dxos/tracing": "0.7.5-main.d9d2d4e",
72
+ "@dxos/util": "0.7.5-main.d9d2d4e"
42
73
  },
43
74
  "devDependencies": {
44
75
  "@types/js-yaml": "^4.0.5",
@@ -0,0 +1,158 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import { expect, test } from 'vitest';
6
+
7
+ import { Config, mapFromKeyValues, mapToKeyValues } from './config';
8
+ // @ts-ignore
9
+ import defaults from './testing/defaults.js';
10
+ // @ts-ignore
11
+ import envmap from './testing/envs-map.js';
12
+
13
+ test('Empty config', () => {
14
+ const config = new Config();
15
+
16
+ expect(config.values).toBeTruthy();
17
+ expect(config.get('runtime.props.title')).toBeUndefined();
18
+ });
19
+
20
+ test('Basic config', () => {
21
+ const config = new Config(
22
+ {
23
+ runtime: {
24
+ props: {
25
+ title: 'testing',
26
+ },
27
+ },
28
+ },
29
+ {
30
+ runtime: {
31
+ app: {
32
+ theme: 'light',
33
+ },
34
+ },
35
+ },
36
+ );
37
+
38
+ expect(config.values).toEqual({
39
+ version: 1,
40
+ runtime: {
41
+ app: {
42
+ theme: 'light',
43
+ },
44
+ props: {
45
+ title: 'testing',
46
+ },
47
+ },
48
+ });
49
+ });
50
+
51
+ test('Runtime and module config', () => {
52
+ const config = new Config(
53
+ {
54
+ package: {
55
+ modules: [
56
+ {
57
+ name: 'example:app/tasks',
58
+ record: {
59
+ web: {
60
+ entryPoint: 'main.js',
61
+ },
62
+ },
63
+ },
64
+ ],
65
+ },
66
+ },
67
+ {
68
+ runtime: {
69
+ services: {
70
+ signaling: [
71
+ {
72
+ server: 'ws://localhost:<random-port>',
73
+ },
74
+ ],
75
+ },
76
+ },
77
+ },
78
+ );
79
+
80
+ expect(config.values).toEqual({
81
+ version: 1,
82
+ package: {
83
+ modules: [
84
+ {
85
+ name: 'example:app/tasks',
86
+ record: {
87
+ web: {
88
+ entryPoint: 'main.js',
89
+ },
90
+ },
91
+ },
92
+ ],
93
+ },
94
+ runtime: {
95
+ services: {
96
+ signaling: [
97
+ {
98
+ server: 'ws://localhost:<random-port>',
99
+ },
100
+ ],
101
+ },
102
+ },
103
+ });
104
+ });
105
+
106
+ test.skip('Mapping', () => {
107
+ process.env.TEST_CLIENT_ID = '900';
108
+ process.env.TEST_SERVER_ENDPOINT = 'http://localhost';
109
+
110
+ const config = new Config(
111
+ {
112
+ runtime: {
113
+ client: {
114
+ tag: 'testing',
115
+ },
116
+ },
117
+ } as any,
118
+ mapFromKeyValues(envmap, process.env),
119
+ );
120
+
121
+ expect(config.values).toEqual({
122
+ runtime: {
123
+ client: {
124
+ id: 900,
125
+ tag: 'testing',
126
+ },
127
+ server: {
128
+ endpoint: 'http://localhost',
129
+ },
130
+ },
131
+ });
132
+
133
+ const values = mapToKeyValues(envmap, config.values);
134
+
135
+ expect(values).toEqual({
136
+ TEST_CLIENT_ID: 900,
137
+ TEST_CLIENT_TAG: 'testing',
138
+ TEST_SERVER_ENDPOINT: 'http://localhost',
139
+ });
140
+ });
141
+
142
+ test.skip('mapToKeyValuesping', () => {
143
+ const config = new Config(
144
+ {
145
+ client: {
146
+ tag: 'testing',
147
+ },
148
+ } as any,
149
+ defaults as any,
150
+ );
151
+
152
+ const values = mapToKeyValues(envmap, config.values);
153
+
154
+ expect(values).toEqual({
155
+ TEST_CLIENT_ID: 123,
156
+ TEST_CLIENT_TAG: 'testing',
157
+ });
158
+ });
package/src/config.ts ADDED
@@ -0,0 +1,182 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import { boolean } from 'boolean';
6
+ import defaultsDeep from 'lodash.defaultsdeep';
7
+ import isMatch from 'lodash.ismatch';
8
+
9
+ import { InvalidConfigError } from '@dxos/protocols';
10
+ import { schema } from '@dxos/protocols/proto';
11
+ import { type Config as ConfigProto } from '@dxos/protocols/proto/dxos/config';
12
+ import { trace } from '@dxos/tracing';
13
+ import { getDeep, setDeep } from '@dxos/util';
14
+
15
+ import { type ConfigKey, type DeepIndex, type ParseKey } from './types';
16
+
17
+ type MappingSpec = Record<string, { path: string; type?: string }>;
18
+ const configRootType = schema.getCodecForType('dxos.config.Config');
19
+
20
+ /**
21
+ * Maps the given objects onto a flattened set of (key x values).
22
+ *
23
+ * Expects parsed yaml content of the form:
24
+ *
25
+ * ```
26
+ * ENV_VAR:
27
+ * path: config.selector.path
28
+ * ```
29
+ *
30
+ * @param {object} spec
31
+ * @param {object} values
32
+ * @return {object}
33
+ */
34
+ export const mapFromKeyValues = (spec: MappingSpec, values: Record<string, any>) => {
35
+ const config = {};
36
+
37
+ for (const [key, { path, type }] of Object.entries(spec)) {
38
+ let value = values[key];
39
+
40
+ if (value !== undefined) {
41
+ if (type) {
42
+ switch (type) {
43
+ case 'boolean': {
44
+ value = boolean(value);
45
+ break;
46
+ }
47
+
48
+ case 'number': {
49
+ value = Number(value);
50
+ break;
51
+ }
52
+
53
+ case 'string': {
54
+ break;
55
+ }
56
+
57
+ case 'json': {
58
+ value = value ? JSON.parse(value) : null;
59
+ break;
60
+ }
61
+
62
+ default: {
63
+ throw new Error(`Invalid type: ${type}`);
64
+ }
65
+ }
66
+ }
67
+
68
+ setDeep(config, path.split('.'), value);
69
+ }
70
+ }
71
+
72
+ return config;
73
+ };
74
+
75
+ /**
76
+ * Maps the given flattend set of (key x values) onto a JSON object.
77
+ * @param {object} spec
78
+ * @param {object} values
79
+ */
80
+ export const mapToKeyValues = (spec: MappingSpec, values: any) => {
81
+ const config: Record<string, any> = {};
82
+
83
+ for (const [key, { path, type }] of Object.entries(spec)) {
84
+ const value = getDeep(values, path.split('.'));
85
+ if (value !== undefined) {
86
+ switch (type) {
87
+ case 'json':
88
+ config[key] = JSON.stringify(value);
89
+ break;
90
+ default:
91
+ config[key] = value;
92
+ }
93
+ }
94
+ }
95
+
96
+ return config;
97
+ };
98
+
99
+ /**
100
+ * Validate config object.
101
+ */
102
+ export const validateConfig = (config: ConfigProto): ConfigProto => {
103
+ if (!('version' in config)) {
104
+ throw new InvalidConfigError('Version not specified');
105
+ }
106
+
107
+ if (config?.version !== 1) {
108
+ throw new InvalidConfigError(`Invalid config version: ${config.version}`);
109
+ }
110
+
111
+ const error = configRootType.protoType.verify(config);
112
+ if (error) {
113
+ throw new InvalidConfigError(error);
114
+ }
115
+
116
+ return config;
117
+ };
118
+
119
+ export const ConfigResource = Symbol.for('dxos.resource.Config');
120
+
121
+ /**
122
+ * Global configuration object.
123
+ * NOTE: Config objects are immutable.
124
+ */
125
+ @trace.resource({ annotation: ConfigResource })
126
+ export class Config {
127
+ private readonly _config: any;
128
+
129
+ /**
130
+ * Creates an immutable instance.
131
+ * @constructor
132
+ */
133
+ constructor(config: ConfigProto = {}, ...objects: ConfigProto[]) {
134
+ this._config = validateConfig(defaultsDeep(config, ...objects, { version: 1 }));
135
+ }
136
+
137
+ /**
138
+ * Returns an immutable config JSON object.
139
+ */
140
+ get values(): ConfigProto {
141
+ return this._config;
142
+ }
143
+
144
+ /**
145
+ * Returns the given config property.
146
+ *
147
+ * @param key A key in the config object. Can be a nested property with keys separated by dots: 'services.signal.server'.
148
+ * @param defaultValue Default value to return if option is not present in the config.
149
+ * @returns The config value or undefined if the option is not present.
150
+ */
151
+ get<K extends ConfigKey>(
152
+ key: K,
153
+ defaultValue?: DeepIndex<ConfigProto, ParseKey<K>>,
154
+ ): DeepIndex<ConfigProto, ParseKey<K>> | undefined {
155
+ return getDeep(this._config, key.split('.')) ?? defaultValue;
156
+ }
157
+
158
+ /**
159
+ * Get unique key.
160
+ */
161
+ find<T = any>(path: string, test: object): T | undefined {
162
+ const values = getDeep(this._config, path.split('.'));
163
+ if (!Array.isArray(values)) {
164
+ return;
165
+ }
166
+
167
+ return values.find((value) => isMatch(value, test));
168
+ }
169
+
170
+ /**
171
+ * Returns the given config property or throw if it doesn't exist.
172
+ *
173
+ * @param key A key in the config object. Can be a nested property with keys separated by dots: 'services.signal.server'.
174
+ */
175
+ getOrThrow<K extends ConfigKey>(key: K): Exclude<DeepIndex<ConfigProto, ParseKey<K>>, undefined> {
176
+ const value: DeepIndex<ConfigProto, ParseKey<K>> | undefined = getDeep(this._config, key.split('.'));
177
+ if (!value) {
178
+ throw new Error(`Config option not present: ${key}`);
179
+ }
180
+ return value;
181
+ }
182
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ // TODO(burdon): Why is this exported? (Rename).
6
+ export * as defs from '@dxos/protocols/proto/dxos/config';
7
+
8
+ export { type Config as ConfigProto } from '@dxos/protocols/proto/dxos/config';
9
+
10
+ export * from './config';
11
+ export * from './loaders';
12
+ export * from './savers';
13
+ export * from './plugin';
14
+ export * from './types';
@@ -0,0 +1,80 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ /* THIS FILE WILL BE LOADED BY CONTEXT REPLACEMENT PLUGIN IN BROWSER ENVS. */
6
+
7
+ /* global __DXOS_CONFIG__ __CONFIG_ENVS__ __CONFIG_DEFAULTS__ __CONFIG_LOCAL__ */
8
+
9
+ import localforage from 'localforage';
10
+
11
+ import { log } from '@dxos/log';
12
+
13
+ const CONFIG_ENDPOINT = '/.well-known/dx/config';
14
+
15
+ export const Local = () => {
16
+ return typeof __CONFIG_LOCAL__ !== 'undefined' ? __CONFIG_LOCAL__ : {};
17
+ };
18
+
19
+ export const Dynamics = async () => {
20
+ const { publicUrl = '', dynamic } = __DXOS_CONFIG__;
21
+ if (!dynamic) {
22
+ log('dynamics disabled');
23
+ return {};
24
+ }
25
+
26
+ log('fetching config...', { publicUrl });
27
+ return await fetch(`${publicUrl}${CONFIG_ENDPOINT}`)
28
+ .then((res) => res.json())
29
+ .catch((error) => {
30
+ log.warn('Failed to fetch dynamic config.', error);
31
+ return {};
32
+ });
33
+ };
34
+
35
+ export const Envs = () => {
36
+ return typeof __CONFIG_ENVS__ !== 'undefined' ? __CONFIG_ENVS__ : {};
37
+ };
38
+
39
+ export const Defaults = () => {
40
+ return typeof __CONFIG_DEFAULTS__ !== 'undefined' ? __CONFIG_DEFAULTS__ : {};
41
+ };
42
+
43
+ /**
44
+ * Settings config from browser storage.
45
+ */
46
+ export const Storage = async () => {
47
+ try {
48
+ const config = await localforage.getItem('dxos.org/settings/config');
49
+ if (config) {
50
+ return config;
51
+ }
52
+ } catch (err) {
53
+ log.warn('Failed to load config', { err });
54
+ }
55
+ return {};
56
+ };
57
+
58
+ export const Remote = (target, authenticationToken) => {
59
+ if (!target) {
60
+ return {};
61
+ }
62
+
63
+ try {
64
+ const url = new URL(target);
65
+ const protocol = url.protocol.slice(0, -1);
66
+
67
+ return {
68
+ runtime: {
69
+ client: {
70
+ // TODO(burdon): Remove vault.html.
71
+ remoteSource: url.origin + (protocol.startsWith('http') ? '/vault.html' : ''),
72
+ remoteSourceAuthenticationToken: authenticationToken,
73
+ },
74
+ },
75
+ };
76
+ } catch (err) {
77
+ log.catch(err);
78
+ return {};
79
+ }
80
+ };
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import yaml from 'js-yaml';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ import { log } from '@dxos/log';
10
+ import { type Config as ConfigProto } from '@dxos/protocols/proto/dxos/config';
11
+
12
+ import { mapFromKeyValues } from '../config';
13
+ import { FILE_DEFAULTS, FILE_ENVS } from '../types';
14
+
15
+ // TODO(burdon): Move code out of index file.
16
+
17
+ const DEFAULT_BASE_PATH = path.resolve(process.cwd(), 'config');
18
+
19
+ const maybeLoadFile = (file: string): any => {
20
+ try {
21
+ return yaml.load(fs.readFileSync(file, { encoding: 'utf8' }));
22
+ } catch (err: any) {
23
+ // Ignored.
24
+ }
25
+ };
26
+
27
+ //
28
+ // NOTE: Export LocalStorage and Dynamics for typescript to typecheck browser code (see ConfigPlugin).
29
+ //
30
+
31
+ /**
32
+ * Profile
33
+ */
34
+ export const Profile = (profile = 'default') => {
35
+ const configFile = path.join(process.env.HOME ?? '~', `.config/dx/profile/${profile}.yml`);
36
+ return maybeLoadFile(configFile) as ConfigProto;
37
+ };
38
+
39
+ /**
40
+ * Development config.
41
+ */
42
+ // TODO(burdon): Rename or reconcile with Profile above?
43
+ export const Local = (): Partial<ConfigProto> => ({});
44
+
45
+ /**
46
+ * Provided dynamically by server.
47
+ */
48
+ export const Dynamics = (): Partial<ConfigProto> => ({});
49
+
50
+ /**
51
+ * ENV variable (key/value) map.
52
+ */
53
+ export const Envs = (basePath = DEFAULT_BASE_PATH): Partial<ConfigProto> => {
54
+ const content = maybeLoadFile(path.resolve(basePath, FILE_ENVS));
55
+ return content ? mapFromKeyValues(content, process.env) : {};
56
+ };
57
+
58
+ /**
59
+ * JSON config.
60
+ */
61
+ export const Defaults = (basePath = DEFAULT_BASE_PATH): Partial<ConfigProto> =>
62
+ maybeLoadFile(path.resolve(basePath, FILE_DEFAULTS)) ?? {};
63
+
64
+ /**
65
+ * Load config from storage.
66
+ */
67
+ export const Storage = async (): Promise<Partial<ConfigProto>> => ({});
68
+
69
+ export const Remote = (target: string | undefined, authenticationToken?: string): Partial<ConfigProto> => {
70
+ if (!target) {
71
+ return {};
72
+ }
73
+
74
+ try {
75
+ const url = new URL(target);
76
+ const protocol = url.protocol.slice(0, -1);
77
+ return {
78
+ runtime: {
79
+ client: {
80
+ // TODO(burdon): Remove vault.html.
81
+ remoteSource: url.origin + (protocol.startsWith('http') ? '/vault.html' : ''),
82
+ remoteSourceAuthenticationToken: authenticationToken,
83
+ },
84
+ },
85
+ };
86
+ } catch (err) {
87
+ log.catch(err);
88
+ return {};
89
+ }
90
+ };
@@ -0,0 +1,94 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import yaml from 'js-yaml';
6
+ import { execSync } from 'node:child_process';
7
+ import { readFileSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+ import pkgUp from 'pkg-up';
10
+
11
+ import { log } from '@dxos/log';
12
+ import { setDeep } from '@dxos/util';
13
+
14
+ import { type ConfigPluginOpts } from './types';
15
+ import { mapFromKeyValues } from '../config';
16
+
17
+ const CWD = process.cwd();
18
+
19
+ export const definitions = ({
20
+ configPath,
21
+ envPath,
22
+ devPath,
23
+ mode = process.env.NODE_ENV,
24
+ publicUrl = '',
25
+ env,
26
+ }: ConfigPluginOpts) => {
27
+ const KEYS_TO_FILE = {
28
+ __CONFIG_DEFAULTS__: configPath?.length ? configPath : resolve(CWD, 'dx.yml'),
29
+ __CONFIG_ENVS__: envPath?.length ? envPath : resolve(CWD, 'dx-env.yml'),
30
+ } as { [key: string]: string };
31
+
32
+ if (mode !== 'production') {
33
+ KEYS_TO_FILE.__CONFIG_LOCAL__ = devPath ?? resolve(CWD, 'dx-local.yml');
34
+ }
35
+
36
+ return Object.entries(KEYS_TO_FILE).reduce(
37
+ (prev, [key, value]) => {
38
+ let content = {};
39
+
40
+ try {
41
+ content = yaml.load(readFileSync(value, 'utf-8')) as any;
42
+
43
+ // Map environment variables to config values.
44
+ if (key === '__CONFIG_ENVS__') {
45
+ content = mapFromKeyValues(content, process.env);
46
+ }
47
+
48
+ if (key === '__CONFIG_DEFAULTS__') {
49
+ // Load app environment variables into default config.
50
+ Object.entries(process.env).forEach(([key, value]) => {
51
+ if (key.startsWith('DX_') || env?.includes(key)) {
52
+ setDeep(content, ['runtime', 'app', 'env', key], value);
53
+ }
54
+ });
55
+
56
+ // Set build info automatically if available.
57
+ try {
58
+ const timestamp = new Date().toISOString();
59
+ const commitHash =
60
+ process.env.DX_COMMIT_HASH ??
61
+ execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).replace('\n', '');
62
+ const packagePath = pkgUp.sync();
63
+ const packageJson = packagePath && JSON.parse(readFileSync(packagePath, 'utf-8'));
64
+ setDeep(content, ['runtime', 'app', 'build', 'timestamp'], timestamp);
65
+ setDeep(content, ['runtime', 'app', 'build', 'commitHash'], commitHash);
66
+ setDeep(content, ['runtime', 'app', 'build', 'version'], packageJson?.version);
67
+ } catch {}
68
+ }
69
+ } catch (err: any) {
70
+ if (err.message.includes('YAMLException')) {
71
+ log.error(`Failed to parse file ${value}:`, err);
72
+ } else {
73
+ log(`Failed to load file ${value}:`, err);
74
+ }
75
+
76
+ if (key === '__CONFIG_DEFAULTS__') {
77
+ // Default config is required.
78
+ throw new Error(`Failed to load default config file from ${value}`);
79
+ }
80
+ }
81
+
82
+ return {
83
+ ...prev,
84
+ [key]: content,
85
+ };
86
+ },
87
+ {
88
+ __DXOS_CONFIG__: { dynamic: mode === 'production', publicUrl },
89
+ __CONFIG_DEFAULTS__: {},
90
+ __CONFIG_ENVS__: {},
91
+ __CONFIG_LOCAL__: {},
92
+ },
93
+ );
94
+ };
@@ -0,0 +1,23 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import type { Plugin } from 'esbuild';
6
+
7
+ import { definitions } from './definitions';
8
+ import { type ConfigPluginOpts } from './types';
9
+
10
+ export const ConfigPlugin = (options: ConfigPluginOpts = {}): Plugin => {
11
+ return {
12
+ name: 'dxos-config',
13
+ setup: ({ initialOptions }) => {
14
+ const esbuildOptions = initialOptions;
15
+ esbuildOptions.define ||= {};
16
+
17
+ Object.entries(definitions({ ...options, mode: process.env.NODE_ENV })).reduce((define, [key, value]) => {
18
+ define[key] = JSON.stringify(value);
19
+ return define;
20
+ }, esbuildOptions.define);
21
+ },
22
+ };
23
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './definitions';
6
+ export * from './types';
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { type Plugin } from 'rollup';
6
+
7
+ import { definitions } from './definitions';
8
+ import { type ConfigPluginOpts } from './types';
9
+
10
+ export const ConfigPlugin = (options: ConfigPluginOpts = {}): Plugin => {
11
+ const contents = Object.entries(definitions({ ...options, mode: process.env.NODE_ENV }))
12
+ .map(([key, value]) => `globalThis.${key} = ${JSON.stringify(value)};`)
13
+ .join('\n');
14
+
15
+ return {
16
+ name: 'dxos-config',
17
+ banner: contents,
18
+ };
19
+ };
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export interface ConfigPluginOpts {
6
+ /**
7
+ * Path to the DX config files, defaults to current working directory.
8
+ */
9
+ configPath?: string;
10
+
11
+ /**
12
+ * Path to the environment variable overrides for DX config.
13
+ */
14
+ envPath?: string;
15
+
16
+ /**
17
+ * Path to the development overrides for DX config.
18
+ */
19
+ devPath?: string;
20
+
21
+ /**
22
+ * If set to `production` then config loaders behave differently:
23
+ * - `Local` returns an empty object
24
+ * - `Dynamics` is enabled and makes an HTTP request to the well known config endpoint for config.
25
+ */
26
+ mode?: string;
27
+
28
+ /**
29
+ * Public URL of the published app. Also used to load the dynamic config.
30
+ *
31
+ * @default ''
32
+ */
33
+ publicUrl?: string;
34
+
35
+ /**
36
+ * Environment variables to be passed to the app.
37
+ */
38
+ env?: string[];
39
+ }
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { resolve } from 'node:path';
6
+ import type { Plugin } from 'vite';
7
+
8
+ import { definitions } from './definitions';
9
+ import { type ConfigPluginOpts } from './types';
10
+
11
+ export const ConfigPlugin = (options: ConfigPluginOpts = {}): Plugin => ({
12
+ name: 'dxos-config',
13
+ config: ({ root }) => {
14
+ const configPath = root && resolve(root, options.configPath ?? 'dx.yml');
15
+ const envPath = root && resolve(root, options.envPath ?? 'dx-env.yml');
16
+ const devPath = root && resolve(root, options.devPath ?? 'dx-local.yml');
17
+ const define = Object.entries(definitions({ ...options, configPath, envPath, devPath })).reduce(
18
+ (define, [key, value]) => {
19
+ define[key] = JSON.stringify(value);
20
+ return define;
21
+ },
22
+ {} as { [key: string]: string },
23
+ );
24
+
25
+ return { define };
26
+ },
27
+ });
@@ -0,0 +1,28 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ /* THIS FILE WILL BE LOADED BY CONTEXT REPLACEMENT PLUGIN IN BROWSER ENVS. */
6
+
7
+ import localforage from 'localforage';
8
+
9
+ import { log } from '@dxos/log';
10
+
11
+ let PERFORMING_CONFIG_SAVE = false;
12
+
13
+ export const SaveConfig = async (config) => {
14
+ if (PERFORMING_CONFIG_SAVE) {
15
+ log.warn('Already performing config save');
16
+ return;
17
+ }
18
+ PERFORMING_CONFIG_SAVE = true;
19
+
20
+ try {
21
+ await localforage.setItem('dxos.org/settings/config', config);
22
+ } catch (err) {
23
+ log.warn('Failed to save config', { err });
24
+ return {};
25
+ } finally {
26
+ PERFORMING_CONFIG_SAVE = false;
27
+ }
28
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Config as ConfigProto } from '@dxos/protocols/proto/dxos/config';
6
+
7
+ export const SaveConfig = async (_: ConfigProto) => {};
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ // This script is used to test whether the esbuild config plugin is properly injecting config constants.
6
+ // TODO(wittjosiah): Automate this test.
7
+
8
+ const { build } = require('esbuild');
9
+ const { join } = require('node:path');
10
+
11
+ // eslint-disable-next-line
12
+ // @ts-ignore
13
+ const { ConfigPlugin } = require('../../dist/plugin/node/esbuild-plugin.cjs');
14
+
15
+ void build({
16
+ entryPoints: [join(__dirname, './entry.js')],
17
+ outdir: join(__dirname, '../../out'),
18
+ write: true,
19
+ bundle: true,
20
+ plugins: [ConfigPlugin()],
21
+ });
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ // TODO(wittjosiah): This should be JSON but isn't due to Nx TS build issue.
6
+ export default {
7
+ client: {
8
+ id: 123,
9
+ tag: 'xyz',
10
+ },
11
+ };
@@ -0,0 +1,10 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ // eslint-disable-next-line
6
+ // @ts-ignore
7
+ const { Defaults } = require('../../dist/lib/node/index.cjs');
8
+
9
+ // eslint-disable-next-line no-console
10
+ console.log(Defaults());
@@ -0,0 +1,17 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ // TODO(wittjosiah): This should be JSON but isn't due to Nx TS build issue.
6
+ export default {
7
+ TEST_CLIENT_ID: {
8
+ path: 'runtime.client.id',
9
+ type: 'number',
10
+ },
11
+ TEST_CLIENT_TAG: {
12
+ path: 'runtime.client.tag',
13
+ },
14
+ TEST_SERVER_ENDPOINT: {
15
+ path: 'runtime.server.endpoint',
16
+ },
17
+ };
package/src/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import { type Config as ConfigProto } from '@dxos/protocols/proto/dxos/config';
6
+
7
+ export const FILE_DEFAULTS = 'defaults.yml';
8
+ export const FILE_ENVS = 'envs-map.yml';
9
+ export const FILE_DYNAMICS = 'config.yml';
10
+
11
+ type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`;
12
+
13
+ /**
14
+ * Returns all dot-separated nested keys for an object.
15
+ *
16
+ * Read more: https://stackoverflow.com/a/68404823.
17
+ */
18
+ type DotNestedKeys<T> = (
19
+ T extends object
20
+ ? {
21
+ [K in Exclude<keyof T, symbol>]: `${K}${DotPrefix<DotNestedKeys<T[K]>>}`;
22
+ }[Exclude<keyof T, symbol>]
23
+ : ''
24
+ ) extends infer D
25
+ ? Extract<D, string>
26
+ : never;
27
+
28
+ /**
29
+ * Parse a dot separated nested key into an array of keys.
30
+ *
31
+ * Example: 'services.signal.server' -> ['services', 'signal', 'server'].
32
+ */
33
+ export type ParseKey<K extends string> = K extends `${infer L}.${infer Rest}` ? [L, ...ParseKey<Rest>] : [K];
34
+
35
+ /**
36
+ * Array of types that can act as an object key.
37
+ */
38
+ type Keys = (keyof any)[];
39
+
40
+ /**
41
+ * Retrieves a property type in a series of nested objects.
42
+ *
43
+ * Read more: https://stackoverflow.com/a/61648690.
44
+ */
45
+ export type DeepIndex<T, KS extends Keys, Fail = undefined> = KS extends [infer F, ...infer R]
46
+ ? F extends keyof Exclude<T, undefined>
47
+ ? R extends Keys
48
+ ? DeepIndex<Exclude<T, undefined>[F], R, Fail>
49
+ : Fail
50
+ : Fail
51
+ : T;
52
+
53
+ /**
54
+ * Any nested dot separated key that can be in config.
55
+ */
56
+ // TODO(egorgripasov): Clean once old config deprecated.
57
+ export type ConfigKey = DotNestedKeys<ConfigProto>;
@@ -1,8 +0,0 @@
1
- import { type Compiler } from 'webpack';
2
- import { type ConfigPluginOpts } from './types';
3
- export declare class ConfigPlugin {
4
- private readonly _options;
5
- constructor(_options?: ConfigPluginOpts);
6
- apply(compiler: Compiler): void;
7
- }
8
- //# sourceMappingURL=webpack-plugin.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"webpack-plugin.d.ts","sourceRoot":"","sources":["../../../../src/plugin/webpack-plugin.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGtD,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,GAAE,gBAAqB;IAE5D,KAAK,CAAC,QAAQ,EAAE,QAAQ;CAWzB"}
@@ -1,5 +0,0 @@
1
- //
2
- // Copyright 2022 DXOS.org
3
- //
4
-
5
- export * from './dist/types/src/plugin/esbuild-plugin';
package/esbuild-plugin.js DELETED
@@ -1,8 +0,0 @@
1
- //
2
- // Copyright 2021 DXOS.org
3
- //
4
-
5
- // Not included in the main `index.ts` so that the apps are not bundled with it.
6
- // It contains native Node functionality so must not be included in the final app bundle, only during compilation/bundling.
7
-
8
- module.exports = require('./dist/plugin/node/esbuild-plugin.cjs');
@@ -1,5 +0,0 @@
1
- //
2
- // Copyright 2022 DXOS.org
3
- //
4
-
5
- export * from './dist/types/src/plugin/rollup-plugin';
package/rollup-plugin.js DELETED
@@ -1,8 +0,0 @@
1
- //
2
- // Copyright 2021 DXOS.org
3
- //
4
-
5
- // Not included in the main `index.ts` so that the apps are not bundled with it.
6
- // It contains native Node functionality so must not be included in the final app bundle, only during compilation/bundling.
7
-
8
- module.exports = require('./dist/plugin/node/rollup-plugin.cjs');
package/vite-plugin.d.ts DELETED
@@ -1,5 +0,0 @@
1
- //
2
- // Copyright 2022 DXOS.org
3
- //
4
-
5
- export * from './dist/types/src/plugin/vite-plugin';
@@ -1,5 +0,0 @@
1
- //
2
- // Copyright 2022 DXOS.org
3
- //
4
-
5
- export * from './dist/types/src/plugin/webpack-plugin';
package/webpack-plugin.js DELETED
@@ -1,8 +0,0 @@
1
- //
2
- // Copyright 2021 DXOS.org
3
- //
4
-
5
- // Not included in the main `index.ts` so that the apps are not bundled with it.
6
- // It contains native Node functionality so must not be included in the final app bundle, only during compilation/bundling.
7
-
8
- module.exports = require('./dist/plugin/node/webpack-plugin.cjs');