wyvrnpm 1.1.1 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyvrnpm",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A simple, static-hosting-compatible C++ package manager",
5
5
  "keywords": [
6
6
  "c++",
@@ -8,6 +8,69 @@ const { readManifest } = require('../manifest');
8
8
  const { getProvider } = require('../providers');
9
9
  const { readConfig } = require('../config');
10
10
 
11
+ /**
12
+ * Parse a .wyvrnignore file and return an array of pattern strings.
13
+ * Lines starting with '#' and blank lines are ignored.
14
+ */
15
+ function loadIgnorePatterns(ignoreFile) {
16
+ if (!fs.existsSync(ignoreFile)) return [];
17
+ return fs
18
+ .readFileSync(ignoreFile, 'utf8')
19
+ .split(/\r?\n/)
20
+ .map((l) => l.trim())
21
+ .filter((l) => l.length > 0 && !l.startsWith('#'));
22
+ }
23
+
24
+ /**
25
+ * Convert a glob-style ignore pattern to a RegExp.
26
+ * Supports '*', '**', '?' wildcards and leading '/' to anchor to root.
27
+ */
28
+ function patternToRegex(pattern) {
29
+ // A leading slash means "anchored to root"; strip it and keep the rest
30
+ const anchored = pattern.startsWith('/');
31
+ const p = anchored ? pattern.slice(1) : pattern;
32
+
33
+ let re = p
34
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars
35
+ .replace(/\*\*/g, '\x00') // placeholder for **
36
+ .replace(/\*/g, '[^/]*') // * → match within segment
37
+ .replace(/\?/g, '[^/]') // ? → single non-slash char
38
+ .replace(/\x00/g, '.*'); // ** → match across segments
39
+
40
+ // If anchored, match from the start; otherwise match any path segment
41
+ return anchored ? new RegExp(`^${re}(/|$)`) : new RegExp(`(^|/)${re}(/|$)`);
42
+ }
43
+
44
+ /**
45
+ * Recursively add files from `dir` into `zip`, skipping any path that
46
+ * matches one of the compiled ignore regexes.
47
+ *
48
+ * @param {AdmZip} zip
49
+ * @param {string} dir Absolute path of the directory being zipped
50
+ * @param {string} base Absolute path of the root publish directory
51
+ * @param {RegExp[]} ignoreRe Compiled ignore patterns
52
+ */
53
+ function addFolderFiltered(zip, dir, base, ignoreRe) {
54
+ for (const entry of fs.readdirSync(dir)) {
55
+ const fullPath = path.join(dir, entry);
56
+ // Relative path from publish root, always using forward slashes
57
+ const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
58
+
59
+ if (ignoreRe.some((re) => re.test(relPath))) {
60
+ console.log(`[wyvrn] Ignoring: ${relPath}`);
61
+ continue;
62
+ }
63
+
64
+ const stat = fs.statSync(fullPath);
65
+ if (stat.isDirectory()) {
66
+ addFolderFiltered(zip, fullPath, base, ignoreRe);
67
+ } else {
68
+ const zipDir = path.dirname(relPath).replace(/\\/g, '/');
69
+ zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
70
+ }
71
+ }
72
+ }
73
+
11
74
  async function publish(argv) {
12
75
  let { source, profile, platform, path: publishPath, token, manifest: manifestArg } = argv;
13
76
 
@@ -74,12 +137,20 @@ async function publish(argv) {
74
137
  `[wyvrn] Publishing ${name}@${version} (${targetPlatform}) via ${provider.constructor.providerName} provider`,
75
138
  );
76
139
 
140
+ // Load .wyvrnignore patterns from the publish directory
141
+ const ignoreFile = path.join(srcDir, '.wyvrnignore');
142
+ const ignorePatterns = loadIgnorePatterns(ignoreFile);
143
+ const ignoreRe = ignorePatterns.map(patternToRegex);
144
+ if (ignorePatterns.length > 0) {
145
+ console.log(`[wyvrn] Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
146
+ }
147
+
77
148
  // Build a temporary zip of the publish directory
78
149
  const tmpZipPath = path.join(os.tmpdir(), `wyvrnpm-${name}-${version}-${Date.now()}.zip`);
79
150
  try {
80
151
  console.log(`[wyvrn] Zipping ${srcDir} ...`);
81
152
  const zip = new AdmZip();
82
- zip.addLocalFolder(srcDir);
153
+ addFolderFiltered(zip, srcDir, srcDir, ignoreRe);
83
154
  zip.writeZip(tmpZipPath);
84
155
 
85
156
  await provider.publish(
@@ -43,11 +43,17 @@ class FileProvider extends BaseProvider {
43
43
  async publish(files, options) {
44
44
  const { source, platform, name, version } = options;
45
45
 
46
- const destDir = path.join(this._resolvePath(source), platform, name, version);
46
+ const packageDir = path.join(this._resolvePath(source), platform, name);
47
+ const destDir = path.join(packageDir, version);
47
48
  fs.mkdirSync(destDir, { recursive: true });
48
49
 
49
50
  this._copy(files.manifest, path.join(destDir, 'wyvrn.json'));
50
51
  this._copy(files.zip, path.join(destDir, 'wyvrn.zip'));
52
+
53
+ // Write latest.json at the package root so consumers can resolve "latest"
54
+ const latestPath = path.join(packageDir, 'latest.json');
55
+ console.log(`[wyvrn] → ${latestPath}`);
56
+ fs.writeFileSync(latestPath, JSON.stringify({ version }), 'utf8');
51
57
  }
52
58
 
53
59
  _copy(src, dest) {
@@ -18,9 +18,13 @@ class HttpProvider extends BaseProvider {
18
18
  const { source, platform, name, version, token } = options;
19
19
  const base = source.replace(/\/$/, '');
20
20
  const urlBase = `${base}/${platform}/${name}/${version}`;
21
+ const packageBase = `${base}/${platform}/${name}`;
21
22
 
22
23
  await this._put(`${urlBase}/wyvrn.json`, files.manifest, 'application/json', token);
23
24
  await this._put(`${urlBase}/wyvrn.zip`, files.zip, 'application/zip', token);
25
+
26
+ // Write latest.json at the package root so consumers can resolve "latest"
27
+ await this._putBuffer(`${packageBase}/latest.json`, Buffer.from(JSON.stringify({ version })), 'application/json', token);
24
28
  }
25
29
 
26
30
  _put(url, filePath, contentType, token) {
@@ -58,6 +62,41 @@ class HttpProvider extends BaseProvider {
58
62
  req.end();
59
63
  });
60
64
  }
65
+
66
+ _putBuffer(url, buffer, contentType, token) {
67
+ return new Promise((resolve, reject) => {
68
+ const parsed = new URL(url);
69
+ const lib = parsed.protocol === 'https:' ? https : http;
70
+
71
+ const headers = {
72
+ 'Content-Type': contentType,
73
+ 'Content-Length': buffer.length,
74
+ };
75
+ if (token) headers['Authorization'] = `Bearer ${token}`;
76
+
77
+ console.log(`[wyvrn] → PUT ${url}`);
78
+ const req = lib.request(
79
+ {
80
+ hostname: parsed.hostname,
81
+ port: parsed.port || undefined,
82
+ path: parsed.pathname,
83
+ method: 'PUT',
84
+ headers,
85
+ },
86
+ (res) => {
87
+ res.resume();
88
+ if (res.statusCode >= 200 && res.statusCode < 300) {
89
+ resolve();
90
+ } else {
91
+ reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode} ${res.statusMessage}`));
92
+ }
93
+ },
94
+ );
95
+ req.on('error', reject);
96
+ req.write(buffer);
97
+ req.end();
98
+ });
99
+ }
61
100
  }
62
101
 
63
102
  module.exports = HttpProvider;
@@ -67,12 +67,18 @@ class S3Provider extends BaseProvider {
67
67
 
68
68
  // Destination key base: [prefix/]platform/name/version
69
69
  const keyBase = [prefix, platform, name, version].filter(Boolean).join('/');
70
+ // Package root (without version) for latest.json
71
+ const packageBase = [prefix, platform, name].filter(Boolean).join('/');
70
72
 
71
73
  const clientConfig = profile ? { credentials: fromSSO({ profile }) } : {};
72
74
  const client = new S3Client(clientConfig);
73
75
 
74
76
  await this._put(client, bucket, `${keyBase}/wyvrn.json`, files.manifest, 'application/json', PutObjectCommand);
75
77
  await this._put(client, bucket, `${keyBase}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
78
+
79
+ // Write latest.json at the package root so consumers can resolve "latest"
80
+ const latestContent = Buffer.from(JSON.stringify({ version }));
81
+ await this._putBuffer(client, bucket, `${packageBase}/latest.json`, latestContent, 'application/json', PutObjectCommand);
76
82
  }
77
83
 
78
84
  async _put(client, bucket, key, filePath, contentType, PutObjectCommand) {
@@ -86,6 +92,18 @@ class S3Provider extends BaseProvider {
86
92
  }),
87
93
  );
88
94
  }
95
+
96
+ async _putBuffer(client, bucket, key, content, contentType, PutObjectCommand) {
97
+ console.log(`[wyvrn] → s3://${bucket}/${key}`);
98
+ await client.send(
99
+ new PutObjectCommand({
100
+ Bucket: bucket,
101
+ Key: key,
102
+ Body: content,
103
+ ContentType: contentType,
104
+ }),
105
+ );
106
+ }
89
107
  }
90
108
 
91
109
  module.exports = S3Provider;
package/src/resolve.js CHANGED
@@ -22,6 +22,36 @@ function compareVersions(a, b) {
22
22
  return 0;
23
23
  }
24
24
 
25
+ /**
26
+ * Resolves the "latest" tag for a package by fetching latest.json from the registry.
27
+ *
28
+ * @param {string} name
29
+ * @param {string[]} packageSources
30
+ * @param {string} platform
31
+ * @param {Function} httpClient
32
+ * @returns {Promise<string>} The resolved version string.
33
+ */
34
+ async function resolveLatestVersion(name, packageSources, platform, httpClient) {
35
+ for (const source of packageSources) {
36
+ const base = source.replace(/\/$/, '');
37
+ const candidates = [
38
+ `${base}/${platform}/${name}/latest.json`,
39
+ `${base}/common/${name}/latest.json`,
40
+ ];
41
+ for (const url of candidates) {
42
+ try {
43
+ const response = await httpClient(url);
44
+ if (!response.ok) continue;
45
+ const data = await response.json();
46
+ if (data && data.version) return data.version;
47
+ } catch {
48
+ // Try next candidate.
49
+ }
50
+ }
51
+ }
52
+ throw new Error(`Could not resolve "latest" version for "${name}" from any source`);
53
+ }
54
+
25
55
  /**
26
56
  * Fetches razer.json for a single package from the first source that responds.
27
57
  *
@@ -96,6 +126,17 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
96
126
  version = lockedVersions.get(name);
97
127
  }
98
128
 
129
+ // Resolve the "latest" tag to a concrete version before proceeding.
130
+ if (version === 'latest') {
131
+ try {
132
+ version = await resolveLatestVersion(name, packageSources, platform, httpClient);
133
+ console.log(`[wyvrn] Resolved "${name}@latest" → ${version}`);
134
+ } catch (err) {
135
+ console.warn(`[wyvrn] Warning: ${err.message}`);
136
+ continue;
137
+ }
138
+ }
139
+
99
140
  if (resolved.has(name)) {
100
141
  const existing = resolved.get(name);
101
142
  if (existing === version) {
@@ -154,5 +195,6 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
154
195
 
155
196
  module.exports = {
156
197
  compareVersions,
198
+ resolveLatestVersion,
157
199
  resolveDependencies,
158
200
  };