@platforma-sdk/package-builder-lib 1.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.
Files changed (134) hide show
  1. package/assets/conda-dockerfile.template +45 -0
  2. package/assets/python-dockerfile.template +22 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +33 -0
  4. package/dist/_virtual/_rolldown/runtime.js +13 -0
  5. package/dist/core/archive.cjs +52 -0
  6. package/dist/core/archive.cjs.map +1 -0
  7. package/dist/core/archive.js +47 -0
  8. package/dist/core/archive.js.map +1 -0
  9. package/dist/core/conda/builder.cjs +226 -0
  10. package/dist/core/conda/builder.cjs.map +1 -0
  11. package/dist/core/conda/builder.js +220 -0
  12. package/dist/core/conda/builder.js.map +1 -0
  13. package/dist/core/conda/validate-spec.cjs +76 -0
  14. package/dist/core/conda/validate-spec.cjs.map +1 -0
  15. package/dist/core/conda/validate-spec.js +73 -0
  16. package/dist/core/conda/validate-spec.js.map +1 -0
  17. package/dist/core/core.cjs +403 -0
  18. package/dist/core/core.cjs.map +1 -0
  19. package/dist/core/core.d.ts +82 -0
  20. package/dist/core/core.d.ts.map +1 -0
  21. package/dist/core/core.js +397 -0
  22. package/dist/core/core.js.map +1 -0
  23. package/dist/core/docker-conda.cjs +75 -0
  24. package/dist/core/docker-conda.cjs.map +1 -0
  25. package/dist/core/docker-conda.js +71 -0
  26. package/dist/core/docker-conda.js.map +1 -0
  27. package/dist/core/docker-python.cjs +147 -0
  28. package/dist/core/docker-python.cjs.map +1 -0
  29. package/dist/core/docker-python.js +144 -0
  30. package/dist/core/docker-python.js.map +1 -0
  31. package/dist/core/docker.cjs +223 -0
  32. package/dist/core/docker.cjs.map +1 -0
  33. package/dist/core/docker.js +207 -0
  34. package/dist/core/docker.js.map +1 -0
  35. package/dist/core/envs.cjs +58 -0
  36. package/dist/core/envs.cjs.map +1 -0
  37. package/dist/core/envs.d.ts +26 -0
  38. package/dist/core/envs.d.ts.map +1 -0
  39. package/dist/core/envs.js +51 -0
  40. package/dist/core/envs.js.map +1 -0
  41. package/dist/core/package-info.cjs +450 -0
  42. package/dist/core/package-info.cjs.map +1 -0
  43. package/dist/core/package-info.d.ts +57 -0
  44. package/dist/core/package-info.d.ts.map +1 -0
  45. package/dist/core/package-info.js +447 -0
  46. package/dist/core/package-info.js.map +1 -0
  47. package/dist/core/paths.cjs +15 -0
  48. package/dist/core/paths.cjs.map +1 -0
  49. package/dist/core/paths.js +14 -0
  50. package/dist/core/paths.js.map +1 -0
  51. package/dist/core/resolver.cjs +99 -0
  52. package/dist/core/resolver.cjs.map +1 -0
  53. package/dist/core/resolver.js +95 -0
  54. package/dist/core/resolver.js.map +1 -0
  55. package/dist/core/schemas/artifacts.cjs +136 -0
  56. package/dist/core/schemas/artifacts.cjs.map +1 -0
  57. package/dist/core/schemas/artifacts.d.ts +280 -0
  58. package/dist/core/schemas/artifacts.d.ts.map +1 -0
  59. package/dist/core/schemas/artifacts.js +122 -0
  60. package/dist/core/schemas/artifacts.js.map +1 -0
  61. package/dist/core/schemas/entrypoints.cjs +43 -0
  62. package/dist/core/schemas/entrypoints.cjs.map +1 -0
  63. package/dist/core/schemas/entrypoints.d.ts +32 -0
  64. package/dist/core/schemas/entrypoints.d.ts.map +1 -0
  65. package/dist/core/schemas/entrypoints.js +41 -0
  66. package/dist/core/schemas/entrypoints.js.map +1 -0
  67. package/dist/core/schemas/sw-json.cjs +111 -0
  68. package/dist/core/schemas/sw-json.cjs.map +1 -0
  69. package/dist/core/schemas/sw-json.js +110 -0
  70. package/dist/core/schemas/sw-json.js.map +1 -0
  71. package/dist/core/storage.cjs +86 -0
  72. package/dist/core/storage.cjs.map +1 -0
  73. package/dist/core/storage.js +83 -0
  74. package/dist/core/storage.js.map +1 -0
  75. package/dist/core/sw-json-render.cjs +327 -0
  76. package/dist/core/sw-json-render.cjs.map +1 -0
  77. package/dist/core/sw-json-render.js +322 -0
  78. package/dist/core/sw-json-render.js.map +1 -0
  79. package/dist/core/util.cjs +313 -0
  80. package/dist/core/util.cjs.map +1 -0
  81. package/dist/core/util.d.ts +66 -0
  82. package/dist/core/util.d.ts.map +1 -0
  83. package/dist/core/util.js +281 -0
  84. package/dist/core/util.js.map +1 -0
  85. package/dist/defaults.cjs +54 -0
  86. package/dist/defaults.cjs.map +1 -0
  87. package/dist/defaults.d.ts +21 -0
  88. package/dist/defaults.d.ts.map +1 -0
  89. package/dist/defaults.js +37 -0
  90. package/dist/defaults.js.map +1 -0
  91. package/dist/index.cjs +31 -0
  92. package/dist/index.cjs.map +1 -0
  93. package/dist/index.d.ts +20 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +12 -0
  96. package/dist/index.js.map +1 -0
  97. package/package.json +56 -0
  98. package/src/__test__/Dockerfile +1 -0
  99. package/src/__test__/docker.test.ts +67 -0
  100. package/src/__test__/package.json +39 -0
  101. package/src/__test__/python-example/main.py +1 -0
  102. package/src/__test__/python-example/requirements.txt +0 -0
  103. package/src/__test__/vite.config.mts +7 -0
  104. package/src/core/__tests__/docker-conda.test.ts +145 -0
  105. package/src/core/__tests__/docker-entrypoint-generation.test.ts +119 -0
  106. package/src/core/__tests__/docker-python.test.ts +374 -0
  107. package/src/core/__tests__/publish-docker-images.test.ts +309 -0
  108. package/src/core/archive.ts +102 -0
  109. package/src/core/conda/builder.test.ts +71 -0
  110. package/src/core/conda/builder.ts +384 -0
  111. package/src/core/conda/validate-spec.test.ts +213 -0
  112. package/src/core/conda/validate-spec.ts +107 -0
  113. package/src/core/core.ts +835 -0
  114. package/src/core/docker-conda.ts +149 -0
  115. package/src/core/docker-python.ts +283 -0
  116. package/src/core/docker.ts +300 -0
  117. package/src/core/envs.ts +31 -0
  118. package/src/core/package-info.test.ts +82 -0
  119. package/src/core/package-info.ts +843 -0
  120. package/src/core/paths.ts +16 -0
  121. package/src/core/resolver.ts +195 -0
  122. package/src/core/schemas/artifacts.test.ts +72 -0
  123. package/src/core/schemas/artifacts.ts +279 -0
  124. package/src/core/schemas/entrypoints.test.ts +171 -0
  125. package/src/core/schemas/entrypoints.ts +150 -0
  126. package/src/core/schemas/sw-json.ts +216 -0
  127. package/src/core/schemas/test-artifacts.ts +362 -0
  128. package/src/core/storage.ts +125 -0
  129. package/src/core/sw-json-render.test.ts +215 -0
  130. package/src/core/sw-json-render.ts +682 -0
  131. package/src/core/util.ts +390 -0
  132. package/src/defaults.ts +38 -0
  133. package/src/index.ts +31 -0
  134. package/src/requirements.txt +1 -0
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ import * as fsp from "node:fs/promises";
6
+ import { validateCondaSpec } from "./validate-spec";
7
+ import * as util from "../util";
8
+
9
+ describe("validateCondaSpec", () => {
10
+ let tempDir: string;
11
+ const mockLogger = util.createLogger();
12
+
13
+ beforeEach(() => {
14
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "validate-conda-spec-test"));
15
+ });
16
+
17
+ afterEach(() => {
18
+ fs.rmSync(tempDir, { recursive: true, force: true });
19
+ });
20
+
21
+ const createSpec = async (content: string): Promise<string> => {
22
+ const specPath = path.join(tempDir, "spec.yaml");
23
+ await fsp.writeFile(specPath, content);
24
+ return specPath;
25
+ };
26
+
27
+ const expectValid = async (content: string) => {
28
+ const specPath = await createSpec(content);
29
+ expect(() => validateCondaSpec(mockLogger, specPath)).not.toThrow();
30
+ };
31
+
32
+ const expectInvalid = async (content: string, errorPattern: RegExp) => {
33
+ const specPath = await createSpec(content);
34
+ expect(() => validateCondaSpec(mockLogger, specPath)).toThrow(errorPattern);
35
+ };
36
+
37
+ describe("allowed channels", () => {
38
+ it("should accept single allowed channel", async () => {
39
+ await expectValid(`
40
+ channels:
41
+ - bioconda
42
+ dependencies:
43
+ - anarci
44
+ `);
45
+ });
46
+
47
+ it("should accept multiple allowed channels", async () => {
48
+ await expectValid(`
49
+ channels:
50
+ - bioconda
51
+ - conda-forge
52
+ dependencies: []
53
+ `);
54
+ });
55
+ });
56
+
57
+ describe("forbidden channels", () => {
58
+ const forbiddenChannels = [
59
+ { name: "main", variations: ["main", "MAIN"] },
60
+ { name: "r", variations: ["r", "R"] },
61
+ { name: "msys2", variations: ["msys2", "Msys2"] },
62
+ ];
63
+
64
+ forbiddenChannels.forEach(({ name, variations }) => {
65
+ variations.forEach((variation) => {
66
+ it(`should reject ${name} channel (${variation})`, async () => {
67
+ await expectInvalid(
68
+ `
69
+ channels:
70
+ - conda-forge
71
+ - ${variation}
72
+ dependencies: []
73
+ `,
74
+ new RegExp(`Forbidden channel '${variation}'`),
75
+ );
76
+ });
77
+ });
78
+ });
79
+ });
80
+
81
+ describe("allowed dependencies", () => {
82
+ it("should accept simple package names", async () => {
83
+ await expectValid(`
84
+ channels:
85
+ - conda-forge
86
+ dependencies:
87
+ - python
88
+ - numpy
89
+ `);
90
+ });
91
+
92
+ it("should accept packages with allowed channel prefix", async () => {
93
+ await expectValid(`
94
+ channels:
95
+ - bioconda
96
+ dependencies:
97
+ - bioconda::anarci
98
+ - conda-forge::python
99
+ `);
100
+ });
101
+ });
102
+
103
+ describe("forbidden dependencies prefixes (case insensitive)", () => {
104
+ const forbiddenPrefixes = [
105
+ { prefix: "main", package: "python" },
106
+ { prefix: "MAIN", package: "stats" },
107
+ { prefix: "r", package: "some-package" },
108
+ { prefix: "R", package: "some-package" },
109
+ { prefix: "msys2", package: "git" },
110
+ { prefix: "Msys2", package: "git" },
111
+ ];
112
+
113
+ forbiddenPrefixes.forEach(({ prefix, package: pkg }) => {
114
+ it(`should reject ${prefix}::${pkg}`, async () => {
115
+ await expectInvalid(
116
+ `
117
+ channels:
118
+ - conda-forge
119
+ dependencies:
120
+ - ${prefix}::${pkg}
121
+ `,
122
+ new RegExp(`Forbidden channel prefix '${prefix}::'`),
123
+ );
124
+ });
125
+ });
126
+ });
127
+
128
+ describe("mixed scenarios", () => {
129
+ it("should accept allowed channels with allowed dependencies", async () => {
130
+ await expectValid(`
131
+ channels:
132
+ - bioconda
133
+ - conda-forge
134
+ dependencies:
135
+ - anarci
136
+ - bioconda::biotools
137
+ - python
138
+ `);
139
+ });
140
+
141
+ it("should reject when both channels and dependencies have violations", async () => {
142
+ const content = `
143
+ channels:
144
+ - main
145
+ - conda-forge
146
+ dependencies:
147
+ - main::python
148
+ - anarci
149
+ `;
150
+ const specPath = await createSpec(content);
151
+
152
+ expect(() => validateCondaSpec(mockLogger, specPath)).toThrow(
153
+ /Forbidden channel 'main'[\s\S]*Forbidden channel prefix 'main::'/,
154
+ );
155
+ });
156
+
157
+ it("should reject multiple violations", async () => {
158
+ await expectInvalid(
159
+ `
160
+ channels:
161
+ - main
162
+ - r
163
+ - msys2
164
+ dependencies:
165
+ - main::python
166
+ - r::stats
167
+ - msys2::git
168
+ `,
169
+ /.+/,
170
+ );
171
+ });
172
+ });
173
+
174
+ describe("edge cases", () => {
175
+ it("should handle empty channels list", async () => {
176
+ await expectValid(`
177
+ channels: []
178
+ dependencies:
179
+ - python
180
+ `);
181
+ });
182
+
183
+ it("should handle empty dependencies list", async () => {
184
+ await expectValid(`
185
+ channels:
186
+ - conda-forge
187
+ dependencies: []
188
+ `);
189
+ });
190
+
191
+ it("should handle specs without dependencies section", async () => {
192
+ await expectValid(`
193
+ channels:
194
+ - conda-forge
195
+ `);
196
+ });
197
+
198
+ it("should throw error for non-existent file", () => {
199
+ expect(() => validateCondaSpec(mockLogger, "/non-existent/path.yaml")).toThrow();
200
+ });
201
+
202
+ it("should handle dependencies with version constraints", async () => {
203
+ await expectValid(`
204
+ channels:
205
+ - conda-forge
206
+ dependencies:
207
+ - python=3.9
208
+ - numpy>=1.20.0
209
+ - matplotlib<3.5
210
+ `);
211
+ });
212
+ });
213
+ });
@@ -0,0 +1,107 @@
1
+ import fs from "node:fs";
2
+ import yaml from "yaml";
3
+ import type winston from "winston";
4
+
5
+ export interface CondaSpec {
6
+ channels?: string[];
7
+ dependencies?: (string | Record<string, unknown>)[];
8
+ name?: string;
9
+ prefix?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ /**
14
+ * Channels that are forbidden because they reference the official Anaconda channel.
15
+ * These channels (main, r, msys2, defaults, anaconda) should be avoided in favor of
16
+ * community-maintained channels like conda-forge to avoid licensing restrictions
17
+ */
18
+ const FORBIDDEN_CHANNELS = ["main", "r", "msys2", "defaults", "anaconda"];
19
+
20
+ /**
21
+ * Validates a conda spec file to ensure it doesn't use forbidden channels
22
+ * @param logger Winston logger instance
23
+ * @param specPath Path to the conda spec file
24
+ * @throws Error if forbidden channels are found
25
+ */
26
+ export function validateCondaSpec(logger: winston.Logger, specPath: string): void {
27
+ logger.debug(`Validating conda spec file: ${specPath}`);
28
+
29
+ if (!fs.existsSync(specPath)) {
30
+ throw new Error(`Conda spec file not found: ${specPath}`);
31
+ }
32
+
33
+ const specContent = fs.readFileSync(specPath, "utf-8");
34
+ const spec = yaml.parse(specContent) as CondaSpec;
35
+
36
+ if (!spec || typeof spec !== "object") {
37
+ throw new Error(`Invalid conda spec file format: ${specPath}`);
38
+ }
39
+
40
+ const errors: string[] = [];
41
+
42
+ if (spec.channels) {
43
+ const channelErrors = validateChannels(spec.channels);
44
+ if (channelErrors.length > 0) {
45
+ errors.push(...channelErrors);
46
+ }
47
+ }
48
+
49
+ if (spec.dependencies) {
50
+ const depErrors = validateDependencies(spec.dependencies);
51
+ if (depErrors.length > 0) {
52
+ errors.push(...depErrors);
53
+ }
54
+ }
55
+
56
+ if (errors.length > 0) {
57
+ throw new Error(`Conda spec file contains forbidden Anaconda channels:\n${errors.join("\n")}`);
58
+ }
59
+
60
+ logger.debug(`Conda spec file validation passed: ${specPath}`);
61
+ }
62
+
63
+ /**
64
+ * Validates that no forbidden channels are in the channels list
65
+ * @param channels Array of channel names
66
+ * @returns Array of error messages for forbidden channels
67
+ */
68
+ function validateChannels(channels: string[]): string[] {
69
+ const errors: string[] = [];
70
+
71
+ for (const channel of channels) {
72
+ const channelLower = channel.toLowerCase().trim();
73
+ if (FORBIDDEN_CHANNELS.includes(channelLower)) {
74
+ errors.push(` - Forbidden channel '${channel}' in channels list`);
75
+ }
76
+ }
77
+
78
+ return errors;
79
+ }
80
+
81
+ /**
82
+ * Validates that no dependencies use forbidden channel prefixes
83
+ * @param dependencies Array of dependencies (can be strings or objects)
84
+ * @returns Array of error messages for forbidden dependencies
85
+ */
86
+ function validateDependencies(dependencies: (string | Record<string, unknown>)[]): string[] {
87
+ const errors: string[] = [];
88
+
89
+ for (const dep of dependencies) {
90
+ // Skip non-string dependencies (they might be pip dependencies, etc.)
91
+ if (typeof dep !== "string") {
92
+ continue;
93
+ }
94
+
95
+ // Check if dependency uses channel prefix (format: channel::package)
96
+ if (dep.includes("::")) {
97
+ const [channel, ..._packageParts] = dep.split("::");
98
+ const channelLower = channel.toLowerCase().trim();
99
+
100
+ if (FORBIDDEN_CHANNELS.includes(channelLower)) {
101
+ errors.push(` - Forbidden channel prefix '${channel}::' in dependency '${dep}'`);
102
+ }
103
+ }
104
+ }
105
+
106
+ return errors;
107
+ }