@microtronics/studio-cli 0.51.0 → 0.52.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.
@@ -139,7 +139,7 @@ function generateAutoIncFromMap(ddeMap, globalDefines, manifest) {
139
139
  /**
140
140
  * generate global dlo defines if there are any
141
141
  */
142
- pwn_section('GLOBAL DEFINES - parsed from /dlo/main.ini');
142
+ pwn_section('GLOBAL DEFINES');
143
143
  for (const [define, value] of Object.entries(globalDefines)) {
144
144
  const valueIsDefine = Object.hasOwn(globalDefines, value);
145
145
  addDefinition(define, `${value}`, valueIsDefine);
@@ -72,7 +72,8 @@ var DefaultFiles;
72
72
  dfilesPath: `${deployDirectoryName}dfiles`,
73
73
  dfilesProperties: `${deployDirectoryName}dfiles/properties.json`,
74
74
  bloPath: `${deployDirectoryName}blo`,
75
- reportTemplate: `${deployDirectoryName}/${DefaultFiles.fileNames.reportTemplate}`
75
+ reportTemplate: `${deployDirectoryName}/${DefaultFiles.fileNames.reportTemplate}`,
76
+ definesTs: `${deployDirectoryName}defines.ts`
76
77
  },
77
78
  dlo: {
78
79
  path: dloDirectoryName,
@@ -86,7 +87,9 @@ var DefaultFiles;
86
87
  dfiles: {
87
88
  path: dfilesDirectoryName,
88
89
  mainDFILES: `${dfilesDirectoryName}/main.dfiles`
89
- }
90
+ },
91
+ appIni: 'app.ini',
92
+ defaultsIni: 'defaults.ini'
90
93
  };
91
94
  /**
92
95
  * Changelog.md
package/out/globals.js CHANGED
@@ -44,49 +44,311 @@ var Globals;
44
44
  ENV["production"] = "production";
45
45
  })(ENV = Globals.ENV || (Globals.ENV = {}));
46
46
  /**
47
- * Read all ini files from the project directory
48
- * @param cwd
49
- * @param fs
47
+ * Read all ini files from the project directory and its dependencies.
48
+ *
49
+ * Merge order (lowest → highest priority):
50
+ * Library dlo/<name>.ini & defaults.ini (from each installed dependency)
51
+ * Project dlo/<name>.ini (library projects only)
52
+ * Project dlo/main.ini (or manifest dlo.iniFile override)
53
+ * Project defaults.ini (library) or app.ini (app/addon/analytics)
54
+ *
55
+ * @param cwd - project root URI
56
+ * @param fs - filesystem abstraction
57
+ * @param logger
50
58
  */
51
- async function read(cwd, fs) {
59
+ async function read(cwd, fs, logger) {
52
60
  const manifest = await manifest_1.Manifest.read(cwd, fs);
53
61
  const allDependencies = await dependencies_1.Dependencies.getAll(manifest);
54
62
  const allDefines = {};
63
+ const sections = {};
64
+ const definesWithSource = {};
65
+ // Library dlo/<name>.ini and default.ini files
55
66
  for (const dependencyId in allDependencies) {
56
67
  const depPath = dependencies_1.Dependencies.dependencyToPath(cwd, dependencyId);
57
68
  const name = dependencies_1.Dependencies.dependencyIdToName(dependencyId);
58
69
  const libIniPath = vscode_uri_1.Utils.joinPath(depPath, DLO_PATH, `${name}.ini`);
59
- const hasIni = await fs.stat(libIniPath);
60
- if (hasIni) {
61
- const iniContent = await readIniFile(fs, libIniPath);
62
- Object.assign(allDefines, iniContent);
63
- }
70
+ await mergeIniFile(fs, libIniPath, allDefines, sections, definesWithSource);
71
+ // defaults.ini files
72
+ const libDefaultsPath = vscode_uri_1.Utils.joinPath(depPath, defaultFiles_1.DefaultFiles.filePaths.defaultsIni);
73
+ await mergeIniFile(fs, libDefaultsPath, allDefines, sections, definesWithSource);
64
74
  }
65
- // load the library projects specific ini file
75
+ // Load the library project's specific ini file (deprecated location)
66
76
  if (manifest_1.Manifest.isLibrary(manifest)) {
67
77
  const libIni = vscode_uri_1.Utils.joinPath(cwd, defaultFiles_1.DefaultFiles.filePaths.dlo.path, `${manifest.name}.ini`);
68
- if (await fs.stat(libIni)) {
69
- const iniContent = await readIniFile(fs, libIni);
70
- Object.assign(allDefines, iniContent);
78
+ if (logger && (await fs.stat(libIni))) {
79
+ logger.warn(`dlo/${manifest.name}.ini is deprecated. Please migrate your defines to defaults.ini in the project root.`);
71
80
  }
81
+ await mergeIniFile(fs, libIni, allDefines, sections, definesWithSource);
82
+ }
83
+ // Load the project's main.ini and overwrite existing defines (deprecated location)
84
+ const mainIniRelPath = manifest.dlo?.iniFile || defaultFiles_1.DefaultFiles.filePaths.dlo.mainINI;
85
+ const mainIni = vscode_uri_1.Utils.joinPath(cwd, mainIniRelPath);
86
+ if (logger && (await fs.stat(mainIni))) {
87
+ logger.warn(`${mainIniRelPath} is deprecated. Please migrate your defines to app.ini in the project root.`);
88
+ }
89
+ await mergeIniFile(fs, mainIni, allDefines, sections, definesWithSource);
90
+ // Project root: defaults.ini (library) or app.ini (app/addon/analytics)
91
+ if (manifest_1.Manifest.isLibrary(manifest)) {
92
+ const defaultsIni = vscode_uri_1.Utils.joinPath(cwd, defaultFiles_1.DefaultFiles.filePaths.defaultsIni);
93
+ await mergeIniFile(fs, defaultsIni, allDefines, sections, definesWithSource);
72
94
  }
73
- // load the projects main.ini and overwrite existing defines
74
- const mainIni = vscode_uri_1.Utils.joinPath(cwd, manifest.dlo?.iniFile || defaultFiles_1.DefaultFiles.filePaths.dlo.mainINI);
75
- if (await fs.stat(mainIni)) {
76
- const iniContent = await readIniFile(fs, mainIni);
77
- Object.assign(allDefines, iniContent);
95
+ else {
96
+ const appIni = vscode_uri_1.Utils.joinPath(cwd, defaultFiles_1.DefaultFiles.filePaths.appIni);
97
+ await mergeIniFile(fs, appIni, allDefines, sections, definesWithSource);
78
98
  }
79
- return allDefines;
99
+ return { flat: allDefines, sections, definesWithSource };
80
100
  }
81
101
  Globals.read = read;
102
+ /**
103
+ * Generate a dist/defines.ts file from the resolved config.
104
+ * The generated file creates/extends globalThis.STUDIO_DEFINES.
105
+ *
106
+ * @param cwd - project root URI
107
+ * @param fs - filesystem abstraction
108
+ * @param resolvedConfig - the resolved config from Globals.read()
109
+ */
110
+ async function exportDefinesTs(cwd, fs, resolvedConfig) {
111
+ const entries = Object.entries(resolvedConfig.flat);
112
+ // Build the StudioDefines interface members
113
+ const interfaceLines = [];
114
+ for (const [key, value] of entries) {
115
+ const entry = resolvedConfig.definesWithSource[key];
116
+ if (entry?.comment) {
117
+ const jsDoc = formatJsDoc(entry.comment);
118
+ for (const docLine of jsDoc.split('\n')) {
119
+ interfaceLines.push(`\t\t${docLine}`);
120
+ }
121
+ }
122
+ const tsType = isNumericValue(value) ? 'number' : 'string';
123
+ interfaceLines.push(`\t\t${key}: ${tsType};`);
124
+ }
125
+ const lines = [
126
+ '// Auto-generated by @microtronics/studio-cli \u2014 DO NOT EDIT',
127
+ 'declare global {',
128
+ '\tinterface StudioDefines {',
129
+ ...interfaceLines,
130
+ '\t}',
131
+ '\tvar STUDIO_DEFINES: StudioDefines;',
132
+ '}',
133
+ '',
134
+ 'globalThis.STUDIO_DEFINES ??= {} as StudioDefines;',
135
+ ''
136
+ ];
137
+ for (const [key, value] of entries) {
138
+ const entry = resolvedConfig.definesWithSource[key];
139
+ if (entry?.comment) {
140
+ lines.push(formatJsDoc(entry.comment));
141
+ }
142
+ if (isNumericValue(value)) {
143
+ lines.push(`globalThis.STUDIO_DEFINES['${key}'] = ${Number(value)};`);
144
+ }
145
+ else {
146
+ const escapedValue = value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
147
+ lines.push(`globalThis.STUDIO_DEFINES['${key}'] = '${escapedValue}';`);
148
+ }
149
+ }
150
+ lines.push('');
151
+ lines.push('export {};');
152
+ lines.push('');
153
+ const definesPath = vscode_uri_1.Utils.joinPath(cwd, defaultFiles_1.DefaultFiles.filePaths.dist.definesTs);
154
+ await fs.writeFile(definesPath, lines.join('\n'));
155
+ }
156
+ Globals.exportDefinesTs = exportDefinesTs;
157
+ /**
158
+ * Replace {{KEY}} template tokens in a string with their resolved values.
159
+ *
160
+ * @param content - the content string with {{KEY}} tokens
161
+ * @param defines - the flat defines map
162
+ * @returns the substituted string and list of unresolved keys
163
+ */
164
+ function replaceDefineTemplates(content, defines) {
165
+ const unresolvedKeys = [];
166
+ const result = content.replace(/\{\{([^}]+)\}\}/g, (_match, key) => {
167
+ const trimmedKey = key.trim();
168
+ if (Object.hasOwn(defines, trimmedKey)) {
169
+ return defines[trimmedKey];
170
+ }
171
+ unresolvedKeys.push(trimmedKey);
172
+ return _match;
173
+ });
174
+ return { result, unresolvedKeys };
175
+ }
176
+ Globals.replaceDefineTemplates = replaceDefineTemplates;
82
177
  })(Globals || (exports.Globals = Globals = {}));
83
178
  /**
84
- * Read given ini file from disk
85
- * @param filePath
86
- * @param fs
179
+ * Classify a trimmed ini-file line into one of the known kinds.
180
+ * Moves boolean-operator branching out of the main loop to reduce cognitive complexity.
181
+ */
182
+ function classifyIniLine(trimmed) {
183
+ if (trimmed === '') {
184
+ return { kind: 'empty' };
185
+ }
186
+ if (trimmed.startsWith('#') || trimmed.startsWith(';')) {
187
+ return { kind: 'comment', text: trimmed.replace(/^[#;]\s?/, '') };
188
+ }
189
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
190
+ return { kind: 'section', name: trimmed.slice(1, -1) };
191
+ }
192
+ const eqIndex = trimmed.indexOf('=');
193
+ if (eqIndex > 0) {
194
+ return { kind: 'keyValue', key: trimmed.slice(0, eqIndex).trim() };
195
+ }
196
+ return { kind: 'other' };
197
+ }
198
+ /**
199
+ * Extract comment-to-key associations from raw ini text.
200
+ * Comments are lines starting with `#` or `;`. Consecutive comment lines
201
+ * are joined with `\n`. An empty line between a comment block and a key
202
+ * breaks the association. Section headers (`[Name]`) do NOT break a pending
203
+ * comment — the comment carries over to the first key inside the section.
204
+ *
205
+ * Returns a map of flattened key → comment text (with `#`/`;` prefix stripped).
87
206
  */
88
- async function readIniFile(fs, filePath) {
207
+ function extractComments(rawText) {
208
+ const result = {};
209
+ const lines = rawText.split(/\r?\n/);
210
+ let pendingCommentLines = [];
211
+ let currentSection = null;
212
+ for (const line of lines) {
213
+ const classified = classifyIniLine(line.trim());
214
+ if (classified.kind === 'empty' || classified.kind === 'other') {
215
+ pendingCommentLines = [];
216
+ continue;
217
+ }
218
+ if (classified.kind === 'comment') {
219
+ pendingCommentLines.push(classified.text);
220
+ continue;
221
+ }
222
+ if (classified.kind === 'section') {
223
+ currentSection = classified.name;
224
+ // Don't reset pendingCommentLines — comment carries to first key
225
+ continue;
226
+ }
227
+ // keyValue
228
+ const flatKey = currentSection ? `${currentSection}_${classified.key}` : classified.key;
229
+ if (pendingCommentLines.length > 0) {
230
+ result[flatKey] = pendingCommentLines.join('\n');
231
+ }
232
+ pendingCommentLines = [];
233
+ }
234
+ return result;
235
+ }
236
+ /**
237
+ * Format a comment string as a JSDoc block.
238
+ * Single-line: \/** comment*\/
239
+ * Multi-line: \/**\n * line1\n * line2\n *\/
240
+ */
241
+ function formatJsDoc(comment) {
242
+ const commentLines = comment.split('\n');
243
+ if (commentLines.length === 1) {
244
+ return `/** ${commentLines[0]} */`;
245
+ }
246
+ const body = commentLines.map(l => ` * ${l}`).join('\n');
247
+ return `/**\n${body}\n */`;
248
+ }
249
+ /**
250
+ * Check whether a string value represents a valid number.
251
+ * Empty strings are NOT considered numeric.
252
+ */
253
+ function isNumericValue(value) {
254
+ return value !== '' && !isNaN(Number(value));
255
+ }
256
+ /**
257
+ * Create a DefineEntry, attaching a comment from the comment map if available.
258
+ */
259
+ function createDefineEntry(stringValue, fileUri, lines, key, sectionName, commentMap) {
260
+ const flatKey = sectionName ? `${sectionName}_${key}` : key;
261
+ const entry = {
262
+ value: stringValue,
263
+ sourceUri: fileUri,
264
+ sourceLine: findLineNumber(lines, key, sectionName)
265
+ };
266
+ if (commentMap[flatKey]) {
267
+ entry.comment = commentMap[flatKey];
268
+ }
269
+ return entry;
270
+ }
271
+ /**
272
+ * Merge all keys from a parsed ini section into the accumulators.
273
+ */
274
+ function mergeSectionEntries(sectionName, sectionData, allDefines, sections, definesWithSource, fileUri, lines, commentMap) {
275
+ if (!sections[sectionName]) {
276
+ sections[sectionName] = {};
277
+ }
278
+ for (const [key, sectionValue] of Object.entries(sectionData)) {
279
+ const flatKey = `${sectionName}_${key}`;
280
+ const stringValue = `${sectionValue}`;
281
+ allDefines[flatKey] = stringValue;
282
+ sections[sectionName][key] = stringValue;
283
+ definesWithSource[flatKey] = createDefineEntry(stringValue, fileUri, lines, key, sectionName, commentMap);
284
+ }
285
+ }
286
+ /**
287
+ * Read and merge a single ini file into the accumulated defines, sections, and source tracking.
288
+ * Supports sections: keys under [SectionName] are flattened as SectionName_keyname.
289
+ *
290
+ * @param fs - filesystem abstraction
291
+ * @param filePath - URI to the ini file
292
+ * @param allDefines - accumulated flat defines (mutated)
293
+ * @param sections - accumulated sectioned config (mutated)
294
+ * @param definesWithSource - accumulated source tracking (mutated)
295
+ */
296
+ async function mergeIniFile(fs, filePath, allDefines, sections, definesWithSource) {
297
+ // early return if the file doesn't exist
298
+ if (!(await fs.stat(filePath))) {
299
+ return;
300
+ }
89
301
  const iniContent = await fs.readFile(filePath);
90
- return ini.parse(convertBlobToString(iniContent));
302
+ const rawText = convertBlobToString(iniContent);
303
+ const parsed = ini.parse(rawText);
304
+ const fileUri = filePath.toString();
305
+ const lines = rawText.split(/\r?\n/);
306
+ const commentMap = extractComments(rawText);
307
+ for (const [sectionOrKey, value] of Object.entries(parsed)) {
308
+ if (typeof value === 'object' && value !== null) {
309
+ mergeSectionEntries(sectionOrKey, value, allDefines, sections, definesWithSource, fileUri, lines, commentMap);
310
+ }
311
+ else {
312
+ const stringValue = `${value}`;
313
+ allDefines[sectionOrKey] = stringValue;
314
+ definesWithSource[sectionOrKey] = createDefineEntry(stringValue, fileUri, lines, sectionOrKey, null, commentMap);
315
+ }
316
+ }
317
+ }
318
+ /**
319
+ * Find the line number (0-based) of a key within an ini file's text lines.
320
+ * If a sectionName is provided, only searches within that section.
321
+ *
322
+ * @param lines - the file split by newlines
323
+ * @param key - the ini key to find
324
+ * @param sectionName - the section to search within, or null for top-level
325
+ * @returns 0-based line number, or 0 if not found
326
+ */
327
+ function findLineNumber(lines, key, sectionName) {
328
+ let inTargetSection = sectionName === null;
329
+ const sectionHeader = sectionName ? `[${sectionName}]` : null;
330
+ const keyPattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
331
+ for (let i = 0; i < lines.length; i++) {
332
+ const trimmed = lines[i].trim();
333
+ if (sectionHeader && trimmed === sectionHeader) {
334
+ inTargetSection = true;
335
+ continue;
336
+ }
337
+ if (inTargetSection && trimmed.startsWith('[') && trimmed.endsWith(']') && sectionName !== null) {
338
+ // Entered a different section
339
+ inTargetSection = false;
340
+ continue;
341
+ }
342
+ if (inTargetSection && keyPattern.test(lines[i])) {
343
+ return i;
344
+ }
345
+ }
346
+ return 0;
347
+ }
348
+ /**
349
+ * Escape a string for use in a RegExp
350
+ */
351
+ function escapeRegex(str) {
352
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
91
353
  }
92
354
  //# sourceMappingURL=globals.js.map
@@ -13,6 +13,7 @@ const compiler_1 = require("../dlo/compiler");
13
13
  const scriptRunner_1 = require("../scriptRunner");
14
14
  const registryAPI_1 = require("../registryAPI");
15
15
  const manifest_1 = require("../manifest");
16
+ const globals_1 = require("../globals");
16
17
  const library_1 = require("./library");
17
18
  const dfiles_1 = require("../dfiles/dfiles");
18
19
  const tarjs_1 = require("@gera2ld/tarjs");
@@ -42,6 +43,15 @@ var Package;
42
43
  async function buildAll(cwd, fs, logger, env, apiToken, selectedParts = true, errorWithDiagnostics) {
43
44
  selectedParts = normalizeSelectedParts(logger, selectedParts);
44
45
  await preBuild(cwd, fs, logger, env, apiToken, selectedParts);
46
+ // Generate dist/defines.ts before POV/BLO builds
47
+ try {
48
+ const resolvedConfig = await globals_1.Globals.read(cwd, fs, logger);
49
+ await globals_1.Globals.exportDefinesTs(cwd, fs, resolvedConfig);
50
+ logger.log('Generated', defaultFiles_1.DefaultFiles.filePaths.dist.definesTs);
51
+ }
52
+ catch (e) {
53
+ logger.warn('Could not generate defines.ts:', e);
54
+ }
45
55
  const mergedDiagnostics = {
46
56
  [apm_1.APM.Part.dde]: [],
47
57
  [apm_1.APM.Part.dlo]: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microtronics/studio-cli",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Microtronics Studio CLI Tool",
5
5
  "main": "./out/api.js",
6
6
  "typings": "./dist/studio-cli.d.ts",