@titannio/webtoolkit-cli 1.3.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +639 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +268 -0
  5. package/dist/bundle-audit.d.ts +7 -0
  6. package/dist/bundle-audit.js +111 -0
  7. package/dist/cleaner.d.ts +15 -0
  8. package/dist/cleaner.js +359 -0
  9. package/dist/config-reference.d.ts +8 -0
  10. package/dist/config-reference.js +805 -0
  11. package/dist/config.d.ts +306 -0
  12. package/dist/config.js +173 -0
  13. package/dist/dev-grid.d.ts +7 -0
  14. package/dist/dev-grid.js +181 -0
  15. package/dist/dev-watch.d.ts +13 -0
  16. package/dist/dev-watch.js +184 -0
  17. package/dist/environment.d.ts +10 -0
  18. package/dist/environment.js +172 -0
  19. package/dist/guard-registry.d.ts +1 -0
  20. package/dist/guard-registry.js +17 -0
  21. package/dist/guard-runner.d.ts +4 -0
  22. package/dist/guard-runner.js +36 -0
  23. package/dist/guards/any-guard.d.ts +16 -0
  24. package/dist/guards/any-guard.js +121 -0
  25. package/dist/guards/assert-no-tests-in-dist.d.ts +1 -0
  26. package/dist/guards/assert-no-tests-in-dist.js +56 -0
  27. package/dist/guards/check-mojibake.d.ts +52 -0
  28. package/dist/guards/check-mojibake.js +378 -0
  29. package/dist/guards/code-pattern-guard.d.ts +71 -0
  30. package/dist/guards/code-pattern-guard.js +654 -0
  31. package/dist/guards/dal-service-repository-check.d.ts +13 -0
  32. package/dist/guards/dal-service-repository-check.js +264 -0
  33. package/dist/guards/dependency-cruiser-guard.d.ts +14 -0
  34. package/dist/guards/dependency-cruiser-guard.js +69 -0
  35. package/dist/guards/documentation-guard.d.ts +3 -0
  36. package/dist/guards/documentation-guard.js +370 -0
  37. package/dist/guards/guard-config.d.ts +19 -0
  38. package/dist/guards/guard-config.js +87 -0
  39. package/dist/guards/internal-link-guard.d.ts +12 -0
  40. package/dist/guards/internal-link-guard.js +272 -0
  41. package/dist/guards/package-surface-guard.d.ts +37 -0
  42. package/dist/guards/package-surface-guard.js +234 -0
  43. package/dist/guards/pnpm-workspace-config.d.ts +2 -0
  44. package/dist/guards/pnpm-workspace-config.js +40 -0
  45. package/dist/guards/rebuild-preflight.d.ts +29 -0
  46. package/dist/guards/rebuild-preflight.js +137 -0
  47. package/dist/guards/repository-hygiene-guard.d.ts +12 -0
  48. package/dist/guards/repository-hygiene-guard.js +70 -0
  49. package/dist/guards/schema-guard.d.ts +10 -0
  50. package/dist/guards/schema-guard.js +160 -0
  51. package/dist/guards/singleton-deps-guard.d.ts +21 -0
  52. package/dist/guards/singleton-deps-guard.js +183 -0
  53. package/dist/guards/tsconfig-guard.d.ts +5 -0
  54. package/dist/guards/tsconfig-guard.js +105 -0
  55. package/dist/guards/workspace-manifest-guard.d.ts +21 -0
  56. package/dist/guards/workspace-manifest-guard.js +210 -0
  57. package/dist/jsdoc-report.d.ts +7 -0
  58. package/dist/jsdoc-report.js +456 -0
  59. package/dist/process.d.ts +20 -0
  60. package/dist/process.js +86 -0
  61. package/dist/ready-service.d.ts +7 -0
  62. package/dist/ready-service.js +135 -0
  63. package/dist/release-gate.d.ts +7 -0
  64. package/dist/release-gate.js +35 -0
  65. package/dist/repo-check.d.ts +7 -0
  66. package/dist/repo-check.js +121 -0
  67. package/dist/tasks.d.ts +17 -0
  68. package/dist/tasks.js +195 -0
  69. package/dist/upgrade.d.ts +10 -0
  70. package/dist/upgrade.js +674 -0
  71. package/dist/validate.d.ts +7 -0
  72. package/dist/validate.js +51 -0
  73. package/dist/workspace-tests.d.ts +33 -0
  74. package/dist/workspace-tests.js +529 -0
  75. package/package.json +57 -0
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Internal Link Guard
4
+ *
5
+ * Prevents internal SPA navigation through raw <a href="...">.
6
+ * For React routes, use <Link to="..."> from react-router-dom.
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { Project, SyntaxKind, } from 'ts-morph';
11
+ import { BASE_JSX_EXTENSIONS, BASE_SOURCE_EXCLUDE_PATTERNS, assertConfiguredScanScope, compilePatterns, hasExtension, isMainModule, loadGuardConfig, resolveProjectPath } from './guard-config.js';
12
+ const colors = {
13
+ reset: '\x1b[0m',
14
+ bright: '\x1b[1m',
15
+ red: '\x1b[31m',
16
+ green: '\x1b[32m',
17
+ yellow: '\x1b[33m',
18
+ blue: '\x1b[34m',
19
+ gray: '\x1b[90m',
20
+ };
21
+ function shouldSkipFile(filePath, excludePatterns) {
22
+ if (!hasExtension(filePath, BASE_JSX_EXTENSIONS))
23
+ return true;
24
+ return excludePatterns.some((pattern) => pattern.test(filePath));
25
+ }
26
+ function collectFiles(rootDir, includePaths, excludePatterns) {
27
+ const files = [];
28
+ function walk(dir) {
29
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
30
+ return;
31
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ const fullPath = path.join(dir, entry.name);
34
+ if (entry.isDirectory()) {
35
+ walk(fullPath);
36
+ continue;
37
+ }
38
+ if (entry.isFile() && !shouldSkipFile(fullPath, excludePatterns)) {
39
+ files.push(fullPath);
40
+ }
41
+ }
42
+ }
43
+ for (const relativePath of includePaths) {
44
+ walk(resolveProjectPath(rootDir, relativePath));
45
+ }
46
+ return files;
47
+ }
48
+ function isInternalHrefLiteral(value) {
49
+ const normalized = value.trim();
50
+ return normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../');
51
+ }
52
+ function isExternalHttpHrefLiteral(value) {
53
+ const normalized = value.trim().toLowerCase();
54
+ return normalized.startsWith('http://') || normalized.startsWith('https://');
55
+ }
56
+ function parseJsxAttributeStaticValue(attr) {
57
+ if (!attr)
58
+ return null;
59
+ const initializer = attr.getInitializer();
60
+ if (!initializer)
61
+ return null;
62
+ if (initializer.getKind() === SyntaxKind.StringLiteral) {
63
+ return initializer.getText().slice(1, -1);
64
+ }
65
+ const expression = initializer.asKindOrThrow(SyntaxKind.JsxExpression).getExpression();
66
+ if (!expression)
67
+ return null;
68
+ if (expression.getKind() === SyntaxKind.StringLiteral || expression.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral) {
69
+ return expression.getText().slice(1, -1);
70
+ }
71
+ return null;
72
+ }
73
+ function inspectHrefAttribute(attr) {
74
+ const initializer = attr.getInitializer();
75
+ const snippet = attr.getText();
76
+ if (!initializer)
77
+ return { kind: 'other', snippet };
78
+ if (initializer.getKind() === SyntaxKind.StringLiteral) {
79
+ const literalValue = initializer.getText().slice(1, -1);
80
+ if (literalValue.trim() === '#') {
81
+ return {
82
+ kind: 'placeholder',
83
+ reason: 'href="#"',
84
+ snippet,
85
+ };
86
+ }
87
+ if (isExternalHttpHrefLiteral(literalValue)) {
88
+ return {
89
+ kind: 'external',
90
+ reason: `href="${literalValue}"`,
91
+ snippet,
92
+ };
93
+ }
94
+ if (isInternalHrefLiteral(literalValue)) {
95
+ return {
96
+ kind: 'internal',
97
+ reason: `href="${literalValue}"`,
98
+ snippet,
99
+ };
100
+ }
101
+ return { kind: 'other', snippet };
102
+ }
103
+ const expression = initializer.asKindOrThrow(SyntaxKind.JsxExpression).getExpression();
104
+ if (!expression)
105
+ return { kind: 'other', snippet };
106
+ if (expression.getKind() === SyntaxKind.StringLiteral || expression.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral) {
107
+ const text = expression.getText();
108
+ const value = text.slice(1, -1);
109
+ if (value.trim() === '#') {
110
+ return {
111
+ kind: 'placeholder',
112
+ reason: `href={${text}}`,
113
+ snippet,
114
+ };
115
+ }
116
+ if (isExternalHttpHrefLiteral(value)) {
117
+ return {
118
+ kind: 'external',
119
+ reason: `href={${text}}`,
120
+ snippet,
121
+ };
122
+ }
123
+ if (isInternalHrefLiteral(value)) {
124
+ return {
125
+ kind: 'internal',
126
+ reason: `href={${text}}`,
127
+ snippet,
128
+ };
129
+ }
130
+ }
131
+ const expressionText = expression.getText().trim();
132
+ const looksLikeInternalRouteExpression = expressionText.includes('ROUTES.') ||
133
+ expressionText.startsWith('`/') ||
134
+ expressionText.startsWith('"/') ||
135
+ expressionText.startsWith("'/") ||
136
+ expressionText.startsWith('`./') ||
137
+ expressionText.startsWith('`../');
138
+ if (looksLikeInternalRouteExpression) {
139
+ return {
140
+ kind: 'internal',
141
+ reason: `href={${expressionText}}`,
142
+ snippet,
143
+ };
144
+ }
145
+ return { kind: 'other', snippet };
146
+ }
147
+ function hasSecureRel(relValue) {
148
+ if (!relValue)
149
+ return false;
150
+ const tokens = new Set(relValue
151
+ .trim()
152
+ .split(/\s+/)
153
+ .map((token) => token.toLowerCase()));
154
+ return tokens.has('noopener') && tokens.has('noreferrer');
155
+ }
156
+ function getAnchorViolationsFromNode(node, sourceRelativePath) {
157
+ if (node.getTagNameNode().getText() !== 'a')
158
+ return [];
159
+ const attrs = node
160
+ .getAttributes()
161
+ .map((attr) => attr.asKind(SyntaxKind.JsxAttribute))
162
+ .filter((attr) => Boolean(attr));
163
+ const hrefAttr = attrs.find((attr) => attr.getNameNode().getText() === 'href');
164
+ const targetAttr = attrs.find((attr) => attr.getNameNode().getText() === 'target');
165
+ const relAttr = attrs.find((attr) => attr.getNameNode().getText() === 'rel');
166
+ const targetValue = parseJsxAttributeStaticValue(targetAttr);
167
+ const relValue = parseJsxAttributeStaticValue(relAttr);
168
+ const violations = [];
169
+ if (targetValue === '_blank' && !hasSecureRel(relValue)) {
170
+ violations.push({
171
+ filePath: sourceRelativePath,
172
+ line: (relAttr ?? targetAttr).getStartLineNumber(),
173
+ hrefSnippet: relAttr?.getText() ?? targetAttr.getText(),
174
+ reason: 'target="_blank" requires rel="noopener noreferrer"',
175
+ });
176
+ }
177
+ if (!hrefAttr)
178
+ return violations;
179
+ const check = inspectHrefAttribute(hrefAttr);
180
+ if (check.kind === 'other')
181
+ return violations;
182
+ if (check.kind === 'internal') {
183
+ violations.push({
184
+ filePath: sourceRelativePath,
185
+ line: hrefAttr.getStartLineNumber(),
186
+ hrefSnippet: check.snippet,
187
+ reason: check.reason,
188
+ });
189
+ return violations;
190
+ }
191
+ if (check.kind === 'placeholder') {
192
+ violations.push({
193
+ filePath: sourceRelativePath,
194
+ line: hrefAttr.getStartLineNumber(),
195
+ hrefSnippet: check.snippet,
196
+ reason: 'placeholder href detected (use button or real route instead of "#")',
197
+ });
198
+ return violations;
199
+ }
200
+ if (targetValue !== '_blank') {
201
+ violations.push({
202
+ filePath: sourceRelativePath,
203
+ line: hrefAttr.getStartLineNumber(),
204
+ hrefSnippet: check.snippet,
205
+ reason: `external link must use target="_blank" (${check.reason})`,
206
+ });
207
+ }
208
+ if (!hasSecureRel(relValue)) {
209
+ violations.push({
210
+ filePath: sourceRelativePath,
211
+ line: hrefAttr.getStartLineNumber(),
212
+ hrefSnippet: relAttr?.getText() ?? check.snippet,
213
+ reason: 'external link must include rel="noopener noreferrer"',
214
+ });
215
+ }
216
+ return violations;
217
+ }
218
+ export async function runInternalLinkGuard(options = {}) {
219
+ const rootDir = options.rootDir ?? process.cwd();
220
+ const config = options.config ?? await loadGuardConfig('internalLink', rootDir);
221
+ const excludePatterns = compilePatterns(config.excludePatterns, BASE_SOURCE_EXCLUDE_PATTERNS);
222
+ /* v8 ignore next -- both outcomes are asserted; V8 omits the fallthrough branch */
223
+ if (config.includePaths.length === 0)
224
+ throw new Error('guards.internalLink.includePaths must not be empty.');
225
+ console.info(`${colors.bright}${colors.blue}🔗 Checking internal navigation links...${colors.reset}`);
226
+ const files = collectFiles(rootDir, config.includePaths, excludePatterns);
227
+ assertConfiguredScanScope({
228
+ root: rootDir,
229
+ guardName: 'internal-link',
230
+ configPath: 'guards.internalLink.includePaths',
231
+ configuredPaths: config.includePaths,
232
+ eligibleFiles: files,
233
+ });
234
+ const project = new Project();
235
+ const violations = [];
236
+ for (const absoluteFilePath of files) {
237
+ const sourceFile = project.addSourceFileAtPath(absoluteFilePath);
238
+ const relativePath = path.relative(rootDir, absoluteFilePath);
239
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).forEach((node) => {
240
+ violations.push(...getAnchorViolationsFromNode(node, relativePath));
241
+ });
242
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach((node) => {
243
+ violations.push(...getAnchorViolationsFromNode(node, relativePath));
244
+ });
245
+ }
246
+ if (violations.length === 0) {
247
+ console.info(`${colors.green}${colors.bright}✅ [OK]${colors.reset} Internal links are router-safe.`);
248
+ return 0;
249
+ }
250
+ console.info(`${colors.bright}${colors.yellow}⚠️ FOUND LINK POLICY VIOLATIONS${colors.reset}`);
251
+ console.info(`${colors.gray}Policies:${colors.reset}`);
252
+ console.info(`${colors.gray} - use <Link to=\"...\"> for internal app navigation${colors.reset}`);
253
+ console.info(`${colors.gray} - do not use href=\"#\" placeholders${colors.reset}`);
254
+ console.info(`${colors.gray} - external http(s) links must use target=\"_blank\" and rel=\"noopener noreferrer\"${colors.reset}\n`);
255
+ for (const violation of violations) {
256
+ console.info(`${colors.gray}${violation.filePath}:${violation.line}${colors.reset}`);
257
+ console.info(` ${colors.red}→${colors.reset} ${violation.reason}`);
258
+ console.info(` ${colors.gray}${violation.hrefSnippet}${colors.reset}\n`);
259
+ }
260
+ console.info(`${colors.bright}${colors.red}⚠️ IMPORTANT: Fix link policy violations before proceeding.${colors.reset}\n`);
261
+ return 1;
262
+ }
263
+ /* v8 ignore start -- executable adapter */
264
+ if (isMainModule(import.meta.url)) {
265
+ runInternalLinkGuard().then((code) => {
266
+ process.exitCode = code;
267
+ }).catch((error) => {
268
+ console.error(error.message);
269
+ process.exitCode = 1;
270
+ });
271
+ }
272
+ /* v8 ignore stop */
@@ -0,0 +1,37 @@
1
+ import type { PackageSurfaceGuardConfig } from '../config.js';
2
+ type PackageManifest = {
3
+ main?: unknown;
4
+ module?: unknown;
5
+ types?: unknown;
6
+ typings?: unknown;
7
+ bin?: unknown;
8
+ exports?: unknown;
9
+ };
10
+ type ManifestTarget = {
11
+ field: string;
12
+ target: string;
13
+ };
14
+ export type PackageSurfaceIssue = {
15
+ packageDirectory: string;
16
+ field: string;
17
+ filePath: string;
18
+ message: string;
19
+ };
20
+ export type PackCommandResult = {
21
+ error?: Error;
22
+ status: number | null;
23
+ stdout: string;
24
+ stderr: string;
25
+ };
26
+ type PackCommand = (packageDirectory: string) => PackCommandResult;
27
+ export declare function collectManifestTargets(packageDirectory: string, manifest: PackageManifest): {
28
+ targets: ManifestTarget[];
29
+ issues: PackageSurfaceIssue[];
30
+ };
31
+ export declare function readPackedFiles(packageDirectory: string, runPack?: PackCommand): string[];
32
+ export declare function runPackageSurfaceGuard(options?: {
33
+ rootDir?: string;
34
+ config?: PackageSurfaceGuardConfig;
35
+ runPack?: PackCommand;
36
+ }): Promise<number>;
37
+ export {};
@@ -0,0 +1,234 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { resolveSpawnSpec } from '../process.js';
5
+ import { compilePatterns, isMainModule, loadGuardConfig, resolveProjectPath, } from './guard-config.js';
6
+ function normalizePackagePath(filePath) {
7
+ return filePath.replaceAll('\\', '/').replace(/^\.\//u, '');
8
+ }
9
+ function childField(parent, key) {
10
+ return /^[A-Za-z_$][\w$]*$/u.test(key)
11
+ ? `${parent}.${key}`
12
+ : `${parent}[${JSON.stringify(key)}]`;
13
+ }
14
+ function unsupportedTarget(packageDirectory, field, value) {
15
+ return {
16
+ packageDirectory,
17
+ field,
18
+ filePath: '',
19
+ message: `unsupported manifest target: ${JSON.stringify(value)}`,
20
+ };
21
+ }
22
+ function collectExports(packageDirectory, field, value, targets, issues) {
23
+ if (value === null)
24
+ return;
25
+ if (typeof value === 'string') {
26
+ targets.push({ field, target: value });
27
+ return;
28
+ }
29
+ if (Array.isArray(value)) {
30
+ value.forEach((entry, index) => {
31
+ collectExports(packageDirectory, `${field}[${index}]`, entry, targets, issues);
32
+ });
33
+ return;
34
+ }
35
+ if (value && typeof value === 'object') {
36
+ for (const [key, entry] of Object.entries(value)) {
37
+ collectExports(packageDirectory, childField(field, key), entry, targets, issues);
38
+ }
39
+ return;
40
+ }
41
+ issues.push(unsupportedTarget(packageDirectory, field, value));
42
+ }
43
+ export function collectManifestTargets(packageDirectory, manifest) {
44
+ const targets = [];
45
+ const issues = [];
46
+ for (const field of ['main', 'module', 'types', 'typings']) {
47
+ const value = manifest[field];
48
+ if (value === undefined)
49
+ continue;
50
+ if (typeof value === 'string')
51
+ targets.push({ field, target: value });
52
+ else
53
+ issues.push(unsupportedTarget(packageDirectory, field, value));
54
+ }
55
+ if (manifest.bin !== undefined) {
56
+ if (typeof manifest.bin === 'string') {
57
+ targets.push({ field: 'bin', target: manifest.bin });
58
+ }
59
+ else if (manifest.bin && typeof manifest.bin === 'object' && !Array.isArray(manifest.bin)) {
60
+ for (const [name, value] of Object.entries(manifest.bin)) {
61
+ const field = childField('bin', name);
62
+ if (typeof value === 'string')
63
+ targets.push({ field, target: value });
64
+ else
65
+ issues.push(unsupportedTarget(packageDirectory, field, value));
66
+ }
67
+ }
68
+ else {
69
+ issues.push(unsupportedTarget(packageDirectory, 'bin', manifest.bin));
70
+ }
71
+ }
72
+ if (manifest.exports !== undefined) {
73
+ collectExports(packageDirectory, 'exports', manifest.exports, targets, issues);
74
+ }
75
+ return { targets, issues };
76
+ }
77
+ function runNpmPack(packageDirectory) {
78
+ const command = resolveSpawnSpec('npm', ['pack', '--dry-run', '--json', '--ignore-scripts']);
79
+ const result = spawnSync(command.command, command.args, {
80
+ cwd: packageDirectory,
81
+ encoding: 'utf8',
82
+ shell: false,
83
+ });
84
+ return {
85
+ error: result.error,
86
+ status: result.status,
87
+ stdout: result.stdout,
88
+ stderr: result.stderr,
89
+ };
90
+ }
91
+ export function readPackedFiles(packageDirectory, runPack = runNpmPack) {
92
+ const result = runPack(packageDirectory);
93
+ if (result.error) {
94
+ throw new Error(`package-surface: npm pack failed in ${packageDirectory}: ${result.error.message}`);
95
+ }
96
+ if (result.status !== 0) {
97
+ const detail = result.stderr.trim();
98
+ throw new Error(`package-surface: npm pack failed in ${packageDirectory}${detail ? `: ${detail}` : '.'}`);
99
+ }
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(result.stdout);
103
+ }
104
+ catch (error) {
105
+ throw new Error(`package-surface: invalid npm pack JSON in ${packageDirectory}: ${error.message}`);
106
+ }
107
+ if (!Array.isArray(parsed) || parsed.length !== 1) {
108
+ throw new Error(`package-surface: npm pack JSON in ${packageDirectory} must contain one package.`);
109
+ }
110
+ const files = parsed[0].files;
111
+ if (!Array.isArray(files) || files.some((entry) => (!entry || typeof entry !== 'object' || typeof entry.path !== 'string'))) {
112
+ throw new Error(`package-surface: npm pack JSON in ${packageDirectory} has an invalid files list.`);
113
+ }
114
+ return files.map((entry) => normalizePackagePath(entry.path));
115
+ }
116
+ function loadManifest(rootDir, configuredDirectory) {
117
+ const absoluteDirectory = resolveProjectPath(rootDir, configuredDirectory);
118
+ if (!fs.existsSync(absoluteDirectory) || !fs.statSync(absoluteDirectory).isDirectory()) {
119
+ throw new Error(`package-surface: missing package directory: ${configuredDirectory}`);
120
+ }
121
+ const manifestPath = path.join(absoluteDirectory, 'package.json');
122
+ if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile()) {
123
+ throw new Error(`package-surface: package.json is missing in ${configuredDirectory}`);
124
+ }
125
+ let parsed;
126
+ try {
127
+ parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
128
+ }
129
+ catch (error) {
130
+ throw new Error(`package-surface: invalid ${configuredDirectory}/package.json: ${error.message}`);
131
+ }
132
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
133
+ throw new Error(`package-surface: ${configuredDirectory}/package.json must contain an object.`);
134
+ }
135
+ return { absoluteDirectory, manifest: parsed };
136
+ }
137
+ function inspectTarget(packageDirectory, absoluteDirectory, target, packedFiles) {
138
+ if (target.target.includes('*')) {
139
+ return [{
140
+ packageDirectory,
141
+ field: target.field,
142
+ filePath: target.target,
143
+ message: 'wildcard manifest targets are unsupported',
144
+ }];
145
+ }
146
+ if (target.field.startsWith('exports') && !target.target.startsWith('./')) {
147
+ return [{
148
+ packageDirectory,
149
+ field: target.field,
150
+ filePath: target.target,
151
+ message: 'export target must start with ./',
152
+ }];
153
+ }
154
+ let absoluteTarget;
155
+ try {
156
+ absoluteTarget = resolveProjectPath(absoluteDirectory, target.target);
157
+ }
158
+ catch (error) {
159
+ return [{
160
+ packageDirectory,
161
+ field: target.field,
162
+ filePath: target.target,
163
+ message: error.message,
164
+ }];
165
+ }
166
+ const filePath = normalizePackagePath(target.target);
167
+ const issues = [];
168
+ if (!fs.existsSync(absoluteTarget) || !fs.statSync(absoluteTarget).isFile()) {
169
+ issues.push({
170
+ packageDirectory,
171
+ field: target.field,
172
+ filePath,
173
+ message: 'public target is missing after build',
174
+ });
175
+ }
176
+ if (!packedFiles.has(filePath)) {
177
+ issues.push({
178
+ packageDirectory,
179
+ field: target.field,
180
+ filePath,
181
+ message: 'public target is excluded from the npm package',
182
+ });
183
+ }
184
+ return issues;
185
+ }
186
+ export async function runPackageSurfaceGuard(options = {}) {
187
+ const rootDir = options.rootDir ?? process.cwd();
188
+ const config = options.config ?? await loadGuardConfig('packageSurface', rootDir);
189
+ const forbidden = compilePatterns(config.forbiddenPublishedPatterns);
190
+ const issues = [];
191
+ for (const packageDirectory of config.packageDirectories) {
192
+ const loaded = loadManifest(rootDir, packageDirectory);
193
+ const collected = collectManifestTargets(packageDirectory, loaded.manifest);
194
+ const packedFiles = readPackedFiles(loaded.absoluteDirectory, options.runPack);
195
+ const packedSet = new Set(packedFiles);
196
+ issues.push(...collected.issues);
197
+ for (const target of collected.targets) {
198
+ issues.push(...inspectTarget(packageDirectory, loaded.absoluteDirectory, target, packedSet));
199
+ }
200
+ for (const filePath of packedFiles) {
201
+ const index = forbidden.findIndex((pattern) => pattern.test(filePath));
202
+ if (index >= 0) {
203
+ issues.push({
204
+ packageDirectory,
205
+ field: 'npm pack',
206
+ filePath,
207
+ message: `matches forbidden pattern ${config.forbiddenPublishedPatterns[index]}`,
208
+ });
209
+ }
210
+ }
211
+ }
212
+ issues.sort((left, right) => ([left.packageDirectory, left.filePath, left.field, left.message].join('\0')
213
+ .localeCompare([right.packageDirectory, right.filePath, right.field, right.message].join('\0'))));
214
+ if (issues.length === 0) {
215
+ console.info(`Package surface is valid (${config.packageDirectories.length} packages).`);
216
+ return 0;
217
+ }
218
+ console.error('Package surface guard failed:');
219
+ for (const issue of issues) {
220
+ const filePath = issue.filePath ? ` ${issue.filePath}` : '';
221
+ console.error(` - ${issue.packageDirectory} [${issue.field}]${filePath}: ${issue.message}`);
222
+ }
223
+ return 1;
224
+ }
225
+ /* v8 ignore start -- executable adapter */
226
+ if (isMainModule(import.meta.url)) {
227
+ runPackageSurfaceGuard().then((code) => {
228
+ process.exitCode = code;
229
+ }).catch((error) => {
230
+ console.error(error.message);
231
+ process.exitCode = 1;
232
+ });
233
+ }
234
+ /* v8 ignore stop */
@@ -0,0 +1,2 @@
1
+ export declare const PNPM_WORKSPACE_CONFIG_PATH: string;
2
+ export declare function readPnpmWorkspaceOverrides(filePath?: string): Record<string, string>;
@@ -0,0 +1,40 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const PNPM_WORKSPACE_CONFIG_PATH = path.resolve(process.cwd(), 'pnpm-workspace.yaml');
4
+ function unquoteYamlScalar(value) {
5
+ const trimmed = value.trim();
6
+ if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
7
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
8
+ return trimmed.slice(1, -1);
9
+ }
10
+ return trimmed;
11
+ }
12
+ export function readPnpmWorkspaceOverrides(filePath = PNPM_WORKSPACE_CONFIG_PATH) {
13
+ if (!fs.existsSync(filePath)) {
14
+ return {};
15
+ }
16
+ const content = fs.readFileSync(filePath, 'utf8');
17
+ const lines = content.split(/\r?\n/);
18
+ const overrides = {};
19
+ let insideOverrides = false;
20
+ for (const line of lines) {
21
+ if (!insideOverrides) {
22
+ if (/^overrides:\s*$/.test(line)) {
23
+ insideOverrides = true;
24
+ }
25
+ continue;
26
+ }
27
+ if (/^\S/.test(line)) {
28
+ break;
29
+ }
30
+ const match = line.match(/^\s{2}([^:#][^:]*):\s*(.*?)\s*$/);
31
+ if (!match)
32
+ continue;
33
+ const packageName = unquoteYamlScalar(match[1]);
34
+ const version = unquoteYamlScalar(match[2]);
35
+ if (packageName && version) {
36
+ overrides[packageName] = version;
37
+ }
38
+ }
39
+ return overrides;
40
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import type { RebuildPreflightTargetConfig } from '../config.js';
3
+ type TurboTask = {
4
+ task: string;
5
+ command: string;
6
+ package: string;
7
+ cache?: {
8
+ status?: string;
9
+ };
10
+ };
11
+ type TurboDryRunReport = {
12
+ tasks: TurboTask[];
13
+ };
14
+ export type RebuildPreflightReport = {
15
+ target: string;
16
+ warningTitle: string;
17
+ packagesNeedingRebuild: string[];
18
+ };
19
+ type RebuildPreflightOptions = {
20
+ repoRoot?: string;
21
+ target: string;
22
+ };
23
+ export declare function getTargetDefinition(target: string, targetDefinitions: Record<string, RebuildPreflightTargetConfig>): RebuildPreflightTargetConfig;
24
+ export declare function parseTurboDryRun(stdout: string): TurboDryRunReport;
25
+ export declare function extractPackagesNeedingRebuild(report: TurboDryRunReport, relevantBuildPackages: string[]): string[];
26
+ export declare function getRebuildPreflightReport({ repoRoot, target, }: RebuildPreflightOptions): Promise<RebuildPreflightReport>;
27
+ export declare function formatRebuildPreflightWarning(report: RebuildPreflightReport): string;
28
+ export declare function printRebuildPreflightWarning(options: RebuildPreflightOptions): Promise<RebuildPreflightReport>;
29
+ export {};