@platforma-sdk/tengo-builder 2.0.2 → 2.1.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.
@@ -1,4 +1,4 @@
1
- import { parseSource } from './source';
1
+ import { newGetSoftwareInfoRE, newGetTemplateIdRE, parseSource } from './source';
2
2
  import { createLogger } from './util';
3
3
  import {
4
4
  testLocalLib1Name,
@@ -9,6 +9,9 @@ import {
9
9
  testLocalTpl3Src,
10
10
  testLocalTpl3Name
11
11
  } from './test.artifacts';
12
+ import { parseSingleSourceLine } from './source';
13
+ import { expect, describe, test } from 'vitest';
14
+ import { ConsoleLoggerAdapter } from '@milaboratories/ts-helpers';
12
15
 
13
16
  test('test lib 1 parsing', () => {
14
17
  const logger = createLogger('error');
@@ -47,3 +50,323 @@ test('test tpl 3 parsing', () => {
47
50
  const tplSrc = parseSource(logger, 'dist', testLocalTpl3Src, testLocalTpl3Name, true);
48
51
  expect(tplSrc.compilerOptions[0].name).toEqual('hash_override');
49
52
  });
53
+
54
+ describe('parseSingleSourceLine', () => {
55
+ test.each([
56
+ {
57
+ name: 'empty line',
58
+ line: ' ',
59
+ context: {
60
+ isInCommentBlock: false,
61
+ canDetectOptions: true,
62
+ lineNo: 1,
63
+ tplDepREs: new Map()
64
+ },
65
+ localPackageName: 'test-package',
66
+ expected: {
67
+ line: ' ',
68
+ artifact: undefined,
69
+ option: undefined,
70
+ context: {
71
+ isInCommentBlock: false,
72
+ canDetectOptions: true,
73
+ lineNo: 1,
74
+ tplDepREs: new Map()
75
+ }
76
+ }
77
+ },
78
+ {
79
+ name: 'single-line comment',
80
+ line: '// This is a comment',
81
+ context: {
82
+ isInCommentBlock: false,
83
+ canDetectOptions: true,
84
+ lineNo: 1,
85
+ tplDepREs: new Map()
86
+ },
87
+ localPackageName: 'test-package',
88
+ expected: {
89
+ line: '',
90
+ artifact: undefined,
91
+ option: undefined,
92
+ context: {
93
+ isInCommentBlock: false,
94
+ canDetectOptions: true,
95
+ lineNo: 1,
96
+ tplDepREs: new Map()
97
+ }
98
+ }
99
+ },
100
+ {
101
+ name: 'start of multi-line comment',
102
+ line: '/* Start comment',
103
+ context: {
104
+ isInCommentBlock: false,
105
+ canDetectOptions: true,
106
+ lineNo: 1,
107
+ tplDepREs: new Map()
108
+ },
109
+ localPackageName: 'test-package',
110
+ expected: {
111
+ line: '',
112
+ artifact: undefined,
113
+ option: undefined,
114
+ context: {
115
+ isInCommentBlock: true,
116
+ canDetectOptions: true,
117
+ lineNo: 1,
118
+ tplDepREs: new Map()
119
+ }
120
+ }
121
+ },
122
+ {
123
+ name: 'line inside comment block',
124
+ line: 'This is inside a comment block',
125
+ context: {
126
+ isInCommentBlock: true,
127
+ canDetectOptions: true,
128
+ lineNo: 1,
129
+ tplDepREs: new Map()
130
+ },
131
+ localPackageName: 'test-package',
132
+ expected: {
133
+ line: '',
134
+ artifact: undefined,
135
+ option: undefined,
136
+ context: {
137
+ isInCommentBlock: true,
138
+ canDetectOptions: true,
139
+ lineNo: 1,
140
+ tplDepREs: new Map()
141
+ }
142
+ }
143
+ },
144
+ {
145
+ name: 'end of multi-line comment',
146
+ line: 'End of comment */',
147
+ context: {
148
+ isInCommentBlock: true,
149
+ canDetectOptions: true,
150
+ lineNo: 1,
151
+ tplDepREs: new Map()
152
+ },
153
+ localPackageName: 'test-package',
154
+ expected: {
155
+ line: '',
156
+ artifact: undefined,
157
+ option: undefined,
158
+ context: {
159
+ isInCommentBlock: false,
160
+ canDetectOptions: true,
161
+ lineNo: 1,
162
+ tplDepREs: new Map()
163
+ }
164
+ }
165
+ },
166
+ {
167
+ name: 'compiler option',
168
+ line: '//tengo:nocheck',
169
+ context: {
170
+ isInCommentBlock: false,
171
+ canDetectOptions: true,
172
+ lineNo: 1,
173
+ tplDepREs: new Map()
174
+ },
175
+ localPackageName: 'test-package',
176
+ expected: {
177
+ line: '//tengo:nocheck',
178
+ artifact: undefined,
179
+ option: { name: 'nocheck', args: [] },
180
+ context: {
181
+ isInCommentBlock: false,
182
+ canDetectOptions: true,
183
+ lineNo: 1,
184
+ tplDepREs: new Map()
185
+ }
186
+ }
187
+ },
188
+ {
189
+ name: 'regular code disables canDetectOptions',
190
+ line: 'const x = 5',
191
+ context: {
192
+ isInCommentBlock: false,
193
+ canDetectOptions: true,
194
+ lineNo: 1,
195
+ tplDepREs: new Map()
196
+ },
197
+ localPackageName: 'test-package',
198
+ expected: {
199
+ line: 'const x = 5',
200
+ artifact: undefined,
201
+ option: undefined,
202
+ context: {
203
+ isInCommentBlock: false,
204
+ canDetectOptions: false,
205
+ lineNo: 1,
206
+ tplDepREs: new Map()
207
+ }
208
+ }
209
+ },
210
+ {
211
+ name: 'malformed compiler option warning',
212
+ line: '// tengo:nocheck',
213
+ context: {
214
+ isInCommentBlock: false,
215
+ canDetectOptions: true,
216
+ lineNo: 1,
217
+ tplDepREs: new Map()
218
+ },
219
+ localPackageName: 'test-package',
220
+ expected: {
221
+ line: '// tengo:nocheck',
222
+ artifact: undefined,
223
+ option: undefined,
224
+ context: {
225
+ isInCommentBlock: false,
226
+ canDetectOptions: true,
227
+ lineNo: 1,
228
+ tplDepREs: new Map()
229
+ },
230
+ warning: true
231
+ }
232
+ },
233
+ {
234
+ name: 'regular import',
235
+ line: 'fmt := import("fmt")',
236
+ context: {
237
+ isInCommentBlock: false,
238
+ canDetectOptions: true,
239
+ lineNo: 1,
240
+ tplDepREs: new Map()
241
+ },
242
+ localPackageName: 'test-package',
243
+ expected: {
244
+ line: 'fmt := import("fmt")',
245
+ artifact: undefined,
246
+ option: undefined,
247
+ context: {
248
+ isInCommentBlock: false,
249
+ canDetectOptions: false,
250
+ lineNo: 1,
251
+ tplDepREs: new Map()
252
+ }
253
+ }
254
+ },
255
+ {
256
+ name: 'library import',
257
+ line: 'myLib := import("test-package:myLib")',
258
+ context: {
259
+ isInCommentBlock: false,
260
+ canDetectOptions: true,
261
+ lineNo: 1,
262
+ tplDepREs: new Map()
263
+ },
264
+ localPackageName: 'test-package',
265
+ expected: {
266
+ line: 'myLib := import("test-package:myLib")',
267
+ artifact: { pkg: 'test-package', id: 'myLib', type: 'library' },
268
+ option: undefined,
269
+ context: {
270
+ isInCommentBlock: false,
271
+ canDetectOptions: false,
272
+ lineNo: 1,
273
+ tplDepREs: new Map()
274
+ }
275
+ }
276
+ },
277
+ {
278
+ name: 'library import with globalize',
279
+ line: 'myLib := import("test-package:myLib")',
280
+ context: {
281
+ isInCommentBlock: false,
282
+ canDetectOptions: true,
283
+ lineNo: 1,
284
+ tplDepREs: new Map()
285
+ },
286
+ localPackageName: 'test-package',
287
+ globalizeImports: true,
288
+ expected: {
289
+ line: 'myLib := import("test-package:myLib")',
290
+ artifact: { pkg: 'test-package', id: 'myLib', type: 'library' },
291
+ option: undefined,
292
+ context: {
293
+ isInCommentBlock: false,
294
+ canDetectOptions: false,
295
+ lineNo: 1,
296
+ tplDepREs: new Map()
297
+ }
298
+ }
299
+ },
300
+ {
301
+ name: 'plapi import sets up template detection',
302
+ line: 'plapi := import("plapi")',
303
+ context: {
304
+ isInCommentBlock: false,
305
+ canDetectOptions: true,
306
+ lineNo: 1,
307
+ tplDepREs: new Map()
308
+ },
309
+ localPackageName: 'test-package',
310
+ expected: {
311
+ line: 'plapi := import("plapi")',
312
+ artifact: undefined,
313
+ option: undefined,
314
+ context: {
315
+ isInCommentBlock: false,
316
+ canDetectOptions: false,
317
+ lineNo: 1,
318
+ tplDepREs: (() => {
319
+ const r = new Map();
320
+ r.set('template', newGetTemplateIdRE('plapi'));
321
+ r.set('software', newGetSoftwareInfoRE('plapi'));
322
+ return r;
323
+ })(),
324
+ }
325
+ }
326
+ },
327
+ {
328
+ name: 'tengo-sdk:ll import sets up template detection',
329
+ line: 'll := import("@milaboratory/tengo-sdk:ll")',
330
+ context: {
331
+ isInCommentBlock: false,
332
+ canDetectOptions: true,
333
+ lineNo: 1,
334
+ tplDepREs: new Map()
335
+ },
336
+ localPackageName: 'test-package',
337
+ expected: {
338
+ line: 'll := import("@milaboratory/tengo-sdk:ll")',
339
+ artifact: {
340
+ id: 'll',
341
+ pkg: '@milaboratory/tengo-sdk',
342
+ type: 'library',
343
+ },
344
+ option: undefined,
345
+ context: {
346
+ isInCommentBlock: false,
347
+ canDetectOptions: false,
348
+ lineNo: 1,
349
+ tplDepREs: (() => {
350
+ const r = new Map();
351
+ r.set('template', newGetTemplateIdRE('ll'));
352
+ r.set('software', newGetSoftwareInfoRE('ll'));
353
+ return r;
354
+ })()
355
+ }
356
+ }
357
+ }
358
+ ])('$name', ({ line, context, localPackageName, globalizeImports, expected }) => {
359
+ const result = parseSingleSourceLine(
360
+ new ConsoleLoggerAdapter(),
361
+ line,
362
+ context,
363
+ localPackageName,
364
+ globalizeImports,
365
+ );
366
+
367
+ expect(result.line).toBe(expected.line);
368
+ expect(result.artifact).toEqual(expected.artifact);
369
+ expect(result.option).toEqual(expected.option);
370
+ expect(result.context).toMatchObject(context);
371
+ });
372
+ });
@@ -1,5 +1,4 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import type winston from 'winston';
3
2
  import {
4
3
  type TypedArtifactName,
5
4
  type FullArtifactName,
@@ -10,6 +9,8 @@ import {
10
9
  } from './package';
11
10
  import type { ArtifactMap } from './artifactset';
12
11
  import { createArtifactNameSet } from './artifactset';
12
+ import { createHash } from 'node:crypto';
13
+ import type { MiLogger } from '@milaboratories/ts-helpers';
13
14
 
14
15
  // matches any valid name in tengo. Don't forget to use '\b' when needed to limit the boundaries!
15
16
  const namePattern = '[_a-zA-Z][_a-zA-Z0-9]*';
@@ -22,10 +23,10 @@ const functionCallRE = (moduleName: string, fnName: string) => {
22
23
  );
23
24
  };
24
25
 
25
- const newGetTemplateIdRE = (moduleName: string) => {
26
+ export const newGetTemplateIdRE = (moduleName: string) => {
26
27
  return functionCallRE(moduleName, 'getTemplateId');
27
28
  };
28
- const newGetSoftwareInfoRE = (moduleName: string) => {
29
+ export const newGetSoftwareInfoRE = (moduleName: string) => {
29
30
  return functionCallRE(moduleName, 'getSoftwareInfo');
30
31
  };
31
32
 
@@ -46,6 +47,9 @@ const inlineCommentRE = /\/\*.*?\*\//g; // .*? = non-greedy search
46
47
  const singlelineCommentRE = /^\s*(\/\/)/;
47
48
  const multilineCommentStartRE = /^\s*\/\*/;
48
49
  const multilineCommentEndRE = /\*\//;
50
+
51
+ // import could only be an assignment in a statement,
52
+ // other ways could break a compilation.
49
53
  const importRE = /\s*:=\s*import\s*\(\s*"(?<moduleName>[^"]+)"\s*\)/;
50
54
  const importNameRE = new RegExp(
51
55
  `\\b(?<importName>${namePattern}(\\.${namePattern})*)${importRE.source}`,
@@ -82,6 +86,8 @@ export class ArtifactSource {
82
86
  public readonly compileMode: CompileMode,
83
87
  /** Full artifact id, including package version */
84
88
  public readonly fullName: FullArtifactName,
89
+ /** Hash of the source code */
90
+ public readonly sourceHash: string,
85
91
  /** Normalized source code */
86
92
  public readonly src: string,
87
93
  /** Path to source file where artifact came from */
@@ -94,7 +100,7 @@ export class ArtifactSource {
94
100
  }
95
101
 
96
102
  export function parseSourceFile(
97
- logger: winston.Logger,
103
+ logger: MiLogger,
98
104
  mode: CompileMode,
99
105
  srcFile: string,
100
106
  fullSourceName: FullArtifactName,
@@ -103,11 +109,19 @@ export function parseSourceFile(
103
109
  const src = readFileSync(srcFile).toString();
104
110
  const { deps, normalized, opts } = parseSourceData(logger, src, fullSourceName, normalize);
105
111
 
106
- return new ArtifactSource(mode, fullSourceName, normalized, srcFile, deps.array, opts);
112
+ return new ArtifactSource(
113
+ mode,
114
+ fullSourceName,
115
+ getSha256(normalized),
116
+ normalized,
117
+ srcFile,
118
+ deps.array,
119
+ opts,
120
+ );
107
121
  }
108
122
 
109
123
  export function parseSource(
110
- logger: winston.Logger,
124
+ logger: MiLogger,
111
125
  mode: CompileMode,
112
126
  src: string,
113
127
  fullSourceName: FullArtifactName,
@@ -115,11 +129,18 @@ export function parseSource(
115
129
  ): ArtifactSource {
116
130
  const { deps, normalized, opts } = parseSourceData(logger, src, fullSourceName, normalize);
117
131
 
118
- return new ArtifactSource(mode, fullSourceName, normalized, '', deps.array, opts);
132
+ return new ArtifactSource(mode, fullSourceName, getSha256(normalized), normalized, '', deps.array, opts);
119
133
  }
120
134
 
135
+ /**
136
+ * Reads src
137
+ * returns normalized source code,
138
+ * gets dependencies from imports,
139
+ * maps imports to global names if globalizeImports is true,
140
+ * and collects compiler options like hashOverride.
141
+ */
121
142
  function parseSourceData(
122
- logger: winston.Logger,
143
+ logger: MiLogger,
123
144
  src: string,
124
145
  fullSourceName: FullArtifactName,
125
146
  globalizeImports: boolean,
@@ -150,21 +171,21 @@ function parseSourceData(
150
171
  parserContext.lineNo++;
151
172
 
152
173
  try {
153
- const result = parseSingleSourceLine(
174
+ const { line: processedLine, context: newContext, artifact, option } = parseSingleSourceLine(
154
175
  logger,
155
176
  line,
156
177
  parserContext,
157
178
  fullSourceName.pkg,
158
179
  globalizeImports,
159
180
  );
160
- processedLines.push(result.line);
161
- parserContext = result.context;
181
+ processedLines.push(processedLine);
182
+ parserContext = newContext;
162
183
 
163
- if (result.artifact) {
164
- dependencySet.add(result.artifact);
184
+ if (artifact) {
185
+ dependencySet.add(artifact);
165
186
  }
166
- if (result.option) {
167
- optionList.push(result.option);
187
+ if (option) {
188
+ optionList.push(option);
168
189
  }
169
190
  } catch (error: unknown) {
170
191
  const err = error as Error;
@@ -186,8 +207,8 @@ interface sourceParserContext {
186
207
  lineNo: number;
187
208
  }
188
209
 
189
- function parseSingleSourceLine(
190
- logger: winston.Logger,
210
+ export function parseSingleSourceLine(
211
+ logger: MiLogger,
191
212
  line: string,
192
213
  context: sourceParserContext,
193
214
  localPackageName: string,
@@ -242,6 +263,7 @@ function parseSingleSourceLine(
242
263
  return { line, context, artifact: undefined, option: undefined };
243
264
  }
244
265
 
266
+ // options could be only at the top of the file.
245
267
  context.canDetectOptions = false;
246
268
 
247
269
  const importInstruction = importRE.exec(line);
@@ -249,6 +271,9 @@ function parseSingleSourceLine(
249
271
  if (importInstruction) {
250
272
  const iInfo = parseImport(line);
251
273
 
274
+ // If we have plapi, ll or assets, then try to parse
275
+ // getTemplateId, getSoftwareInfo, getSoftware and getAsset calls.
276
+
252
277
  if (iInfo.module === 'plapi') {
253
278
  if (!context.tplDepREs.has(iInfo.module)) {
254
279
  context.tplDepREs.set(iInfo.module, [
@@ -304,7 +329,7 @@ function parseSingleSourceLine(
304
329
  }
305
330
 
306
331
  if (context.tplDepREs.size > 0) {
307
- for (const [key, artifactRE] of context.tplDepREs) {
332
+ for (const [_, artifactRE] of context.tplDepREs) {
308
333
  for (const [artifactType, re] of artifactRE) {
309
334
  const match = re.exec(line);
310
335
  if (!match || !match.groups) {
@@ -352,8 +377,8 @@ function parseImport(line: string): ImportInfo {
352
377
  }
353
378
 
354
379
  return {
355
- module: moduleName,
356
- alias: importName,
380
+ module: moduleName, // the module name without wrapping quotes: import("<module>")
381
+ alias: importName, // the name of variable that keeps imported module: <alias> := import("<module>")
357
382
  };
358
383
  }
359
384
 
@@ -383,26 +408,6 @@ function parseArtifactName(
383
408
  return { type: aType, pkg: pkgName ?? localPackageName, id: depID };
384
409
  }
385
410
 
386
- function parseTemplateUse(match: RegExpExecArray, localPackageName: string): TypedArtifactName {
387
- const { templateName } = match.groups!;
388
-
389
- if (!templateName) {
390
- throw Error(`failed to parse 'getTemplateId' statement`);
391
- }
392
-
393
- const depInfo = dependencyRE.exec(templateName);
394
- if (!depInfo || !depInfo.groups) {
395
- throw Error(
396
- `failed to parse dependency name inside 'getTemplateId' statement. The dependency name should have format '<package>:<templateName>'`,
397
- );
398
- }
399
-
400
- const { pkgName, depID } = depInfo.groups;
401
- if (!pkgName || !depID) {
402
- throw Error(
403
- `failed to parse dependency name inside 'getTemplateId' statement. The dependency name should have format '<package>:<templateName>'`,
404
- );
405
- }
406
-
407
- return { type: 'template', pkg: pkgName ?? localPackageName, id: depID };
411
+ export function getSha256(source: string): string {
412
+ return createHash('sha256').update(source).digest('hex');
408
413
  }
@@ -1,5 +1,6 @@
1
- import { Template } from './template';
1
+ import { newTemplateFromContent, newTemplateFromData, Template } from './template';
2
2
  import { formatArtefactNameAndVersion, FullArtifactName } from './package';
3
+ import { test, expect } from 'vitest';
3
4
 
4
5
  test('template serialization / deserialization', () => {
5
6
  const name: FullArtifactName = {
@@ -8,46 +9,53 @@ test('template serialization / deserialization', () => {
8
9
  id: 'the-template',
9
10
  version: '1.2.3'
10
11
  };
11
- const template1 = new Template('dist', name,
12
+ const template1 = newTemplateFromData(
13
+ 'dist',
14
+ name,
12
15
  {
13
- data: {
14
- type: 'pl.tengo-template.v2',
16
+ type: 'pl.tengo-template.v3',
17
+ hashToSource: {
18
+ "asdasd": "src1...",
19
+ "asdasd2": "src2...",
20
+ "asdasd3": "src3...",
21
+ },
22
+ template: {
23
+ sourceHash: "asdasd3",
15
24
  ...formatArtefactNameAndVersion(name),
16
25
  libs: {
17
- '@milaboratory/some-package:the-library': {
26
+ 'asdasd': {
18
27
  name: '@milaboratory/some-package:the-library',
19
28
  version: '1.2.3',
20
- src: 'asdasd'
29
+ sourceHash: 'asdasd'
21
30
  }
22
31
  },
23
32
  templates: {
24
- '@milaboratory/some-package:the-template-1': {
25
- type: 'pl.tengo-template.v2',
33
+ 'asdasd2': {
26
34
  name: '@milaboratory/some-package:the-template-1',
27
35
  version: '1.2.3',
28
36
  libs: {
29
37
  '@milaboratory/some-package:the-library:1.2.4': {
30
38
  name: '@milaboratory/some-package:the-library',
31
39
  version: '1.2.4',
32
- src: 'asdasd'
40
+ sourceHash: 'asdasd2'
33
41
  }
34
42
  },
35
43
  templates: {},
36
44
  software: {},
37
45
  assets: {},
38
- src: 'src 1...'
46
+ sourceHash: 'src 1...'
39
47
  }
40
48
  },
41
49
  software: {},
42
50
  assets: {},
43
- src: 'src 2 ...'
44
- }
51
+ },
45
52
  }
46
53
  );
47
54
 
48
- const template2 = new Template('dist',
55
+ const template2 = newTemplateFromContent(
56
+ 'dist',
49
57
  { type: 'template', pkg: '@milaboratory/some-package', id: 'the-template', version: '1.2.3' },
50
- { content: template1.content }
58
+ template1.content,
51
59
  );
52
60
 
53
61
  console.log('Size: raw', JSON.stringify(template1.data).length, 'compressed', template1.content.byteLength);