@wp-typia/project-tools 0.24.11 → 0.24.13
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/dist/runtime/add/cli-add-block-config.js +7 -15
- package/dist/runtime/add/cli-add-block-package-json.d.ts +5 -0
- package/dist/runtime/add/cli-add-block-package-json.js +32 -0
- package/dist/runtime/add/cli-add-block.js +17 -4
- package/dist/runtime/add/cli-add-workspace-admin-view-scaffold.js +1 -5
- package/dist/runtime/add/cli-add-workspace-hooked-block.js +15 -20
- package/dist/runtime/add/cli-add-workspace-mutation.d.ts +15 -2
- package/dist/runtime/add/cli-add-workspace-mutation.js +10 -4
- package/dist/runtime/add/persistence-rest-artifacts.d.ts +32 -7
- package/dist/runtime/add/persistence-rest-artifacts.js +41 -15
- package/dist/runtime/cli/cli-diagnostics.d.ts +9 -0
- package/dist/runtime/cli/cli-diagnostics.js +8 -0
- package/dist/runtime/cli/cli-init-package-json.js +5 -1
- package/dist/runtime/doctor/cli-doctor-standalone-control-flow.d.ts +38 -0
- package/dist/runtime/doctor/cli-doctor-standalone-control-flow.js +365 -0
- package/dist/runtime/doctor/cli-doctor-standalone-rest.d.ts +21 -0
- package/dist/runtime/doctor/cli-doctor-standalone-rest.js +1354 -0
- package/dist/runtime/doctor/cli-doctor-standalone-shared.d.ts +24 -0
- package/dist/runtime/doctor/cli-doctor-standalone-shared.js +50 -0
- package/dist/runtime/doctor/cli-doctor-standalone.d.ts +32 -0
- package/dist/runtime/doctor/cli-doctor-standalone.js +2594 -0
- package/dist/runtime/doctor/cli-doctor-workspace.d.ts +5 -3
- package/dist/runtime/doctor/cli-doctor-workspace.js +27 -3
- package/dist/runtime/doctor/cli-doctor.d.ts +4 -2
- package/dist/runtime/doctor/cli-doctor.js +4 -2
- package/dist/runtime/shared/json-utils.d.ts +7 -0
- package/dist/runtime/shared/json-utils.js +10 -0
- package/dist/runtime/shared/package-json-types.d.ts +1 -1
- package/dist/runtime/shared/package-versions.d.ts +2 -0
- package/dist/runtime/shared/package-versions.js +2 -0
- package/dist/runtime/shared/php-utils.d.ts +39 -2
- package/dist/runtime/shared/php-utils.js +205 -6
- package/dist/runtime/templates/block-generator-service-core.d.ts +1 -1
- package/dist/runtime/templates/block-generator-service-core.js +9 -3
- package/dist/runtime/templates/block-generator-service-spec.d.ts +5 -0
- package/dist/runtime/templates/built-in-block-artifact-documents.d.ts +1 -1
- package/dist/runtime/templates/built-in-block-artifact-documents.js +1 -1
- package/dist/runtime/templates/built-in-block-artifact-types.js +2 -2
- package/dist/runtime/templates/built-in-block-artifacts.d.ts +1 -1
- package/dist/runtime/templates/built-in-block-artifacts.js +16 -12
- package/dist/runtime/templates/built-in-block-attribute-emitters.d.ts +5 -0
- package/dist/runtime/templates/built-in-block-attribute-emitters.js +113 -8
- package/dist/runtime/templates/built-in-block-attribute-specs.js +4 -9
- package/dist/runtime/templates/built-in-block-code-artifacts.js +6 -1
- package/dist/runtime/templates/built-in-block-code-templates/compound-persistence.d.ts +2 -2
- package/dist/runtime/templates/built-in-block-code-templates/compound-persistence.js +24 -1
- package/dist/runtime/templates/built-in-block-code-templates/persistence.d.ts +2 -2
- package/dist/runtime/templates/built-in-block-code-templates/persistence.js +22 -1
- package/dist/runtime/templates/built-in-block-non-ts-compound-templates.d.ts +2 -2
- package/dist/runtime/templates/built-in-block-non-ts-compound-templates.js +6 -2
- package/dist/runtime/templates/built-in-block-non-ts-persistence-templates.d.ts +2 -2
- package/dist/runtime/templates/built-in-block-non-ts-persistence-templates.js +6 -2
- package/dist/runtime/templates/cli-scaffold-emission.d.ts +7 -1
- package/dist/runtime/templates/cli-scaffold-emission.js +10 -1
- package/dist/runtime/templates/cli-templates.d.ts +47 -0
- package/dist/runtime/templates/cli-templates.js +50 -1
- package/dist/runtime/templates/local-dev-presets.js +1 -1
- package/dist/runtime/templates/scaffold-apply-utils.d.ts +4 -8
- package/dist/runtime/templates/scaffold-apply-utils.js +44 -28
- package/dist/runtime/templates/scaffold-compiler-artifacts.d.ts +17 -0
- package/dist/runtime/templates/scaffold-compiler-artifacts.js +81 -0
- package/dist/runtime/templates/scaffold-onboarding.d.ts +5 -0
- package/dist/runtime/templates/scaffold-onboarding.js +13 -2
- package/dist/runtime/templates/scaffold-template-variable-groups.d.ts +4 -0
- package/dist/runtime/templates/scaffold-template-variable-groups.js +7 -0
- package/dist/runtime/templates/scaffold.d.ts +3 -1
- package/dist/runtime/templates/scaffold.js +2 -1
- package/dist/runtime/templates/starter-manifests.js +1 -1
- package/package.json +5 -5
- package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +1 -1
- package/templates/_shared/compound/persistence/package.json.mustache +1 -0
- package/templates/_shared/persistence/core/package.json.mustache +1 -0
|
@@ -0,0 +1,2594 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { resolveSyncBlockMetadataPaths, } from '@wp-typia/block-runtime/metadata-core';
|
|
6
|
+
import ts from 'typescript';
|
|
7
|
+
import { formatInstallCommand, formatRunScript, inferPackageManagerId, } from '../shared/package-managers.js';
|
|
8
|
+
import { findPhpFunctionCallEnd, findPhpFunctionRange, getPhpCodeBraceDepth, hasPhpFunctionCall, hasPhpFunctionCallWithStringArguments, } from '../shared/php-utils.js';
|
|
9
|
+
import { readJsonFileSync } from '../shared/json-utils.js';
|
|
10
|
+
import { createDoctorCheck, createDoctorScopeCheck, } from './cli-doctor-workspace-shared.js';
|
|
11
|
+
import { containsCompletion, countOuterParserContinues, getSafeErrorConstruction, hasCanonicalRuntimeImports, hasCanonicalUnknownFlagThrow, hasEarlierAbruptCompletion, hasOnlyCanonicalParserEffects, isAllowedSyncHelperTopLevelStatement, isCanonicalSyncMainCatchHandler, unwrapStaticExpression, } from './cli-doctor-standalone-control-flow.js';
|
|
12
|
+
import { hasTypeScriptSyntaxErrors, isProjectLocalRelativePath, isSafeProjectRelativePath, } from './cli-doctor-standalone-shared.js';
|
|
13
|
+
import { checkStandaloneRestArtifacts, parseStandaloneRestConfig, STANDALONE_PERSISTENCE_BLOCK_SCHEMA_ARTIFACTS, standaloneProjectRequiresRest, } from './cli-doctor-standalone-rest.js';
|
|
14
|
+
const STANDALONE_SYNC_SCRIPT = path.join('scripts', 'sync-types-to-block-json.ts');
|
|
15
|
+
const STANDALONE_SYNC_PROJECT_SCRIPT = path.join('scripts', 'sync-project.ts');
|
|
16
|
+
const STANDALONE_SYNC_REST_SCRIPT = path.join('scripts', 'sync-rest-contracts.ts');
|
|
17
|
+
const STANDALONE_INDEX_FILE = path.join('src', 'index.tsx');
|
|
18
|
+
const STANDALONE_EDIT_FILE = path.join('src', 'edit.tsx');
|
|
19
|
+
const STANDALONE_SAVE_FILE = path.join('src', 'save.tsx');
|
|
20
|
+
const STANDALONE_TYPES_FILE = path.join('src', 'types.ts');
|
|
21
|
+
const STANDALONE_VALIDATORS_FILE = path.join('src', 'validators.ts');
|
|
22
|
+
const STANDALONE_BLOCK_JSON_FILE = 'src/block.json';
|
|
23
|
+
const STANDALONE_MANIFEST_FILE = 'src/typia.manifest.json';
|
|
24
|
+
const STANDALONE_COMMON_SOURCE_FILES = [
|
|
25
|
+
STANDALONE_SYNC_SCRIPT,
|
|
26
|
+
STANDALONE_SYNC_PROJECT_SCRIPT,
|
|
27
|
+
STANDALONE_INDEX_FILE,
|
|
28
|
+
STANDALONE_EDIT_FILE,
|
|
29
|
+
STANDALONE_TYPES_FILE,
|
|
30
|
+
STANDALONE_SAVE_FILE,
|
|
31
|
+
STANDALONE_VALIDATORS_FILE,
|
|
32
|
+
path.join('src', 'hooks.ts'),
|
|
33
|
+
path.join('src', 'block-metadata.ts'),
|
|
34
|
+
path.join('src', 'manifest-document.ts'),
|
|
35
|
+
path.join('src', 'manifest-defaults-document.ts'),
|
|
36
|
+
path.join('src', 'validator-toolkit.ts'),
|
|
37
|
+
path.join('src', 'style.scss'),
|
|
38
|
+
];
|
|
39
|
+
const STANDALONE_BASIC_SOURCE_FILES = [
|
|
40
|
+
path.join('src', 'editor.scss'),
|
|
41
|
+
path.join('src', 'render.php'),
|
|
42
|
+
];
|
|
43
|
+
const STANDALONE_INTERACTIVITY_SOURCE_FILES = [
|
|
44
|
+
path.join('src', 'editor.scss'),
|
|
45
|
+
path.join('src', 'interactivity.ts'),
|
|
46
|
+
path.join('src', 'interactivity-store.ts'),
|
|
47
|
+
];
|
|
48
|
+
const STANDALONE_PERSISTENCE_SOURCE_FILES = [
|
|
49
|
+
STANDALONE_SYNC_REST_SCRIPT,
|
|
50
|
+
path.join('src', 'api-types.ts'),
|
|
51
|
+
path.join('src', 'api-validators.ts'),
|
|
52
|
+
path.join('src', 'api.ts'),
|
|
53
|
+
path.join('src', 'data.ts'),
|
|
54
|
+
path.join('src', 'transport.ts'),
|
|
55
|
+
path.join('src', 'interactivity.ts'),
|
|
56
|
+
path.join('src', 'render.php'),
|
|
57
|
+
];
|
|
58
|
+
// WordPress core's get_file_data() reads the first 8 KiB for plugin headers.
|
|
59
|
+
const WORDPRESS_PLUGIN_HEADER_SCAN_BYTES = 8 * 1024;
|
|
60
|
+
// Mirrors get_file_data()'s `[ \t\/*#@]*` header prefix. Its zero-length
|
|
61
|
+
// match is intentional: WordPress recognizes a bare `Plugin Name:` line too.
|
|
62
|
+
const WORDPRESS_PLUGIN_NAME_HEADER_PATTERN = /^[\t \/*#@]*Plugin Name[\t ]*:[\t ]*\S[^\r\n]*$/imu;
|
|
63
|
+
const REQUIRED_RUNTIME_PACKAGES = [
|
|
64
|
+
'@wp-typia/block-runtime',
|
|
65
|
+
'@wp-typia/block-types',
|
|
66
|
+
'typia',
|
|
67
|
+
];
|
|
68
|
+
const REQUIRED_WORDPRESS_RUNTIME_PACKAGES = [
|
|
69
|
+
'@wordpress/block-editor',
|
|
70
|
+
'@wordpress/blocks',
|
|
71
|
+
'@wordpress/components',
|
|
72
|
+
'@wordpress/element',
|
|
73
|
+
'@wordpress/i18n',
|
|
74
|
+
];
|
|
75
|
+
const REQUIRED_INTERACTIVITY_RUNTIME_PACKAGES = [
|
|
76
|
+
'@wordpress/interactivity',
|
|
77
|
+
];
|
|
78
|
+
const REQUIRED_REST_RUNTIME_PACKAGES = [
|
|
79
|
+
'@wp-typia/rest',
|
|
80
|
+
'@wp-typia/api-client',
|
|
81
|
+
];
|
|
82
|
+
const REQUIRED_REST_WORDPRESS_RUNTIME_PACKAGES = [
|
|
83
|
+
'@wordpress/api-fetch',
|
|
84
|
+
'@wordpress/data',
|
|
85
|
+
];
|
|
86
|
+
const REQUIRED_INSTALLED_PACKAGES = [
|
|
87
|
+
{
|
|
88
|
+
diagnosticName: '@wp-typia/block-runtime/metadata-core',
|
|
89
|
+
packageName: '@wp-typia/block-runtime',
|
|
90
|
+
resolutionSpecifier: '@wp-typia/block-runtime/metadata-core',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
diagnosticName: '@wp-typia/block-types',
|
|
94
|
+
packageName: '@wp-typia/block-types',
|
|
95
|
+
resolutionSpecifier: '@wp-typia/block-types',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
diagnosticName: 'typia',
|
|
99
|
+
packageName: 'typia',
|
|
100
|
+
resolutionSpecifier: 'typia',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
diagnosticName: 'typescript',
|
|
104
|
+
packageName: 'typescript',
|
|
105
|
+
resolutionSpecifier: 'typescript',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
diagnosticName: 'tsx',
|
|
109
|
+
packageName: 'tsx',
|
|
110
|
+
resolutionSpecifier: 'tsx/cli',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
diagnosticName: '@wordpress/scripts',
|
|
114
|
+
packageName: '@wordpress/scripts',
|
|
115
|
+
resolutionSpecifier: '@wordpress/scripts/bin/wp-scripts.js',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
diagnosticName: '@typia/unplugin/webpack',
|
|
119
|
+
packageName: '@typia/unplugin',
|
|
120
|
+
resolutionSpecifier: '@typia/unplugin/webpack',
|
|
121
|
+
},
|
|
122
|
+
// Resolve manifests so import-only packages such as
|
|
123
|
+
// @wordpress/interactivity remain verifiable through createRequire().
|
|
124
|
+
...REQUIRED_WORDPRESS_RUNTIME_PACKAGES.map((packageName) => ({
|
|
125
|
+
diagnosticName: packageName,
|
|
126
|
+
packageName,
|
|
127
|
+
resolutionSpecifier: `${packageName}/package.json`,
|
|
128
|
+
})),
|
|
129
|
+
];
|
|
130
|
+
const REQUIRED_INTERACTIVITY_INSTALLED_PACKAGES = REQUIRED_INTERACTIVITY_RUNTIME_PACKAGES.map((packageName) => ({
|
|
131
|
+
diagnosticName: packageName,
|
|
132
|
+
packageName,
|
|
133
|
+
resolutionSpecifier: `${packageName}/package.json`,
|
|
134
|
+
}));
|
|
135
|
+
const REQUIRED_REST_INSTALLED_PACKAGES = [
|
|
136
|
+
{
|
|
137
|
+
diagnosticName: '@wp-typia/rest',
|
|
138
|
+
packageName: '@wp-typia/rest',
|
|
139
|
+
resolutionSpecifier: '@wp-typia/rest',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
diagnosticName: '@wp-typia/api-client',
|
|
143
|
+
packageName: '@wp-typia/api-client',
|
|
144
|
+
resolutionSpecifier: '@wp-typia/api-client',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
diagnosticName: '@wp-typia/rest/react',
|
|
148
|
+
packageName: '@wp-typia/rest',
|
|
149
|
+
resolutionSpecifier: '@wp-typia/rest/react',
|
|
150
|
+
},
|
|
151
|
+
...REQUIRED_REST_WORDPRESS_RUNTIME_PACKAGES.map((packageName) => ({
|
|
152
|
+
diagnosticName: packageName,
|
|
153
|
+
packageName,
|
|
154
|
+
resolutionSpecifier: `${packageName}/package.json`,
|
|
155
|
+
})),
|
|
156
|
+
];
|
|
157
|
+
/** Stable codes emitted by standalone-scaffold doctor rows. */
|
|
158
|
+
export const STANDALONE_DOCTOR_CODES = {
|
|
159
|
+
ARTIFACTS: 'wp-typia.standalone.generated-artifacts',
|
|
160
|
+
BOOTSTRAP: 'wp-typia.standalone.bootstrap',
|
|
161
|
+
DEPENDENCIES: 'wp-typia.standalone.dependencies',
|
|
162
|
+
PACKAGE: 'wp-typia.standalone.package',
|
|
163
|
+
SOURCE_LAYOUT: 'wp-typia.standalone.source-layout',
|
|
164
|
+
};
|
|
165
|
+
const WORDPRESS_REST_METHOD_CONSTANTS = {
|
|
166
|
+
DELETE: 'DELETABLE',
|
|
167
|
+
GET: 'READABLE',
|
|
168
|
+
PATCH: 'EDITABLE',
|
|
169
|
+
POST: 'CREATABLE',
|
|
170
|
+
PUT: 'EDITABLE',
|
|
171
|
+
};
|
|
172
|
+
function getDeclaredDependency(packageJson, packageName) {
|
|
173
|
+
return (packageJson.dependencies?.[packageName] ??
|
|
174
|
+
packageJson.devDependencies?.[packageName]);
|
|
175
|
+
}
|
|
176
|
+
function getSafePackageBaseName(packageName) {
|
|
177
|
+
const match = /^(?:@[a-z0-9][a-z0-9._-]*\/)?([a-z0-9][a-z0-9._-]*)$/iu.exec(packageName);
|
|
178
|
+
return match?.[1] ?? null;
|
|
179
|
+
}
|
|
180
|
+
function isStandaloneScaffoldCandidate(projectDir, packageJson) {
|
|
181
|
+
if (packageJson.wpTypia?.projectType === 'workspace') {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (fs.existsSync(path.join(projectDir, 'src', 'blocks'))) {
|
|
185
|
+
// Compound and workspace scaffolds share sync scripts and dependencies
|
|
186
|
+
// with the single-block template, but their canonical source boundary is
|
|
187
|
+
// src/blocks/* rather than root src/types.ts + src/save.tsx.
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const syncTypesScript = packageJson.scripts?.['sync-types'];
|
|
191
|
+
const syncSurfaceSignals = [
|
|
192
|
+
typeof syncTypesScript === 'string' &&
|
|
193
|
+
syncTypesScript.includes('sync-types-to-block-json'),
|
|
194
|
+
fs.existsSync(path.join(projectDir, STANDALONE_SYNC_SCRIPT)),
|
|
195
|
+
fs.existsSync(path.join(projectDir, STANDALONE_TYPES_FILE)),
|
|
196
|
+
fs.existsSync(path.join(projectDir, STANDALONE_SAVE_FILE)),
|
|
197
|
+
].filter(Boolean).length;
|
|
198
|
+
const dependencySignals = [
|
|
199
|
+
'@wp-typia/block-runtime',
|
|
200
|
+
'@wp-typia/block-types',
|
|
201
|
+
].filter((packageName) => typeof getDeclaredDependency(packageJson, packageName) === 'string').length;
|
|
202
|
+
return (syncSurfaceSignals >= 3 ||
|
|
203
|
+
(syncSurfaceSignals >= 2 && dependencySignals >= 1));
|
|
204
|
+
}
|
|
205
|
+
function standaloneProjectRequiresInteractivity(project, requiresRest) {
|
|
206
|
+
if (requiresRest ||
|
|
207
|
+
typeof getDeclaredDependency(project.packageJson, '@wordpress/interactivity') === 'string' ||
|
|
208
|
+
fs.existsSync(path.join(project.projectDir, 'src', 'interactivity.ts')) ||
|
|
209
|
+
fs.existsSync(path.join(project.projectDir, 'src', 'interactivity-store.ts'))) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const blockJson = readJsonFileSync(path.join(project.projectDir, STANDALONE_BLOCK_JSON_FILE), { context: 'standalone block metadata' });
|
|
214
|
+
const viewScriptModules = Array.isArray(blockJson.viewScriptModule)
|
|
215
|
+
? blockJson.viewScriptModule
|
|
216
|
+
: [blockJson.viewScriptModule];
|
|
217
|
+
return viewScriptModules.includes('file:./interactivity.js');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Resolve the nearest supported type-derived standalone scaffold.
|
|
225
|
+
*
|
|
226
|
+
* Detection deliberately combines generated sync/layout signals with wp-typia
|
|
227
|
+
* dependency signals. Requiring every signal would make a damaged scaffold
|
|
228
|
+
* fall back to the misleading environment-only scope, while the weighted
|
|
229
|
+
* threshold avoids treating an arbitrary WordPress package as generated merely
|
|
230
|
+
* because it contains one similarly named file or dependency.
|
|
231
|
+
*
|
|
232
|
+
* @throws {Error} When the nearest package manifest cannot be parsed.
|
|
233
|
+
*/
|
|
234
|
+
export function tryResolveStandaloneScaffoldProject(startDir) {
|
|
235
|
+
let currentDir = path.resolve(startDir);
|
|
236
|
+
while (true) {
|
|
237
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
238
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
239
|
+
const packageJson = readJsonFileSync(packageJsonPath, { context: 'standalone scaffold package manifest' });
|
|
240
|
+
if (isStandaloneScaffoldCandidate(currentDir, packageJson)) {
|
|
241
|
+
return {
|
|
242
|
+
packageJson,
|
|
243
|
+
packageName: typeof packageJson.name === 'string' &&
|
|
244
|
+
packageJson.name.trim().length > 0
|
|
245
|
+
? packageJson.name.trim()
|
|
246
|
+
: path.basename(currentDir),
|
|
247
|
+
projectDir: currentDir,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// A package manifest establishes the nearest project boundary. Do not
|
|
251
|
+
// let an unrelated standalone scaffold in an ancestor directory claim
|
|
252
|
+
// a nested repository or package that has its own manifest.
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const parentDir = path.dirname(currentDir);
|
|
256
|
+
if (parentDir === currentDir) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
currentDir = parentDir;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function getObjectPropertyString(objectLiteral, propertyName) {
|
|
263
|
+
const property = getObjectPropertyAssignment(objectLiteral, propertyName);
|
|
264
|
+
return property && ts.isStringLiteralLike(property.initializer)
|
|
265
|
+
? property.initializer.text
|
|
266
|
+
: undefined;
|
|
267
|
+
}
|
|
268
|
+
function getObjectPropertyAssignment(objectLiteral, propertyName) {
|
|
269
|
+
// Iterate in reverse so the last-defined property wins, matching JavaScript
|
|
270
|
+
// object-literal duplicate-key semantics.
|
|
271
|
+
for (const property of [...objectLiteral.properties].reverse()) {
|
|
272
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const name = property.name;
|
|
276
|
+
const resolvedName = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
277
|
+
if (resolvedName !== propertyName) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
return property;
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
function hasUnsupportedSyncOptionSyntax(objectLiteral) {
|
|
285
|
+
return objectLiteral.properties.some((property) => !ts.isPropertyAssignment(property) ||
|
|
286
|
+
ts.isComputedPropertyName(property.name));
|
|
287
|
+
}
|
|
288
|
+
function getCanonicalSyncImportBindings(sourceFile) {
|
|
289
|
+
const bindings = new Set();
|
|
290
|
+
for (const statement of sourceFile.statements) {
|
|
291
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
292
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
293
|
+
statement.moduleSpecifier.text !==
|
|
294
|
+
'@wp-typia/block-runtime/metadata-core' ||
|
|
295
|
+
statement.importClause?.isTypeOnly ||
|
|
296
|
+
!statement.importClause?.namedBindings ||
|
|
297
|
+
!ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
301
|
+
if (element.isTypeOnly) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const importedName = (element.propertyName ?? element.name).text;
|
|
305
|
+
if (importedName === 'runSyncBlockMetadata') {
|
|
306
|
+
bindings.add(element.name.text);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return bindings;
|
|
311
|
+
}
|
|
312
|
+
function bindingNameContains(name, bindings) {
|
|
313
|
+
if (!name) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
if (ts.isIdentifier(name)) {
|
|
317
|
+
return bindings.has(name.text);
|
|
318
|
+
}
|
|
319
|
+
return name.elements.some((element) => !ts.isOmittedExpression(element) &&
|
|
320
|
+
bindingNameContains(element.name, bindings));
|
|
321
|
+
}
|
|
322
|
+
function hasShadowedBinding(root, bindings) {
|
|
323
|
+
let shadowed = false;
|
|
324
|
+
function visit(node) {
|
|
325
|
+
if (shadowed || ts.isImportDeclaration(node)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
let declaredName;
|
|
329
|
+
if (ts.isVariableDeclaration(node) || ts.isParameter(node)) {
|
|
330
|
+
declaredName = node.name;
|
|
331
|
+
}
|
|
332
|
+
else if (ts.isFunctionDeclaration(node) ||
|
|
333
|
+
ts.isFunctionExpression(node) ||
|
|
334
|
+
ts.isClassDeclaration(node) ||
|
|
335
|
+
ts.isClassExpression(node) ||
|
|
336
|
+
ts.isEnumDeclaration(node) ||
|
|
337
|
+
(ts.isImportEqualsDeclaration(node) && !node.isTypeOnly)) {
|
|
338
|
+
declaredName = node.name;
|
|
339
|
+
}
|
|
340
|
+
if (bindingNameContains(declaredName, bindings)) {
|
|
341
|
+
shadowed = true;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
ts.forEachChild(node, visit);
|
|
345
|
+
}
|
|
346
|
+
visit(root);
|
|
347
|
+
return shadowed;
|
|
348
|
+
}
|
|
349
|
+
function hasImportedBinding(sourceFile, bindingName) {
|
|
350
|
+
return sourceFile.statements.some((statement) => {
|
|
351
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
352
|
+
!statement.importClause ||
|
|
353
|
+
statement.importClause.isTypeOnly) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
const { name, namedBindings } = statement.importClause;
|
|
357
|
+
if (name?.text === bindingName) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
361
|
+
return namedBindings.name.text === bindingName;
|
|
362
|
+
}
|
|
363
|
+
return (namedBindings !== undefined &&
|
|
364
|
+
ts.isNamedImports(namedBindings) &&
|
|
365
|
+
namedBindings.elements.some((element) => !element.isTypeOnly && element.name.text === bindingName));
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const TRUSTED_SYNC_HELPER_GLOBALS = new Set([
|
|
369
|
+
'Error',
|
|
370
|
+
'Object',
|
|
371
|
+
'console',
|
|
372
|
+
'process',
|
|
373
|
+
]);
|
|
374
|
+
const SYNC_TYPES_RUNTIME_IMPORTS = new Map([
|
|
375
|
+
[
|
|
376
|
+
'@wp-typia/block-runtime/metadata-core',
|
|
377
|
+
{ namedBindings: new Set(['runSyncBlockMetadata']) },
|
|
378
|
+
],
|
|
379
|
+
]);
|
|
380
|
+
const SYNC_PROJECT_RUNTIME_IMPORTS = new Map([
|
|
381
|
+
['node:child_process', { namedBindings: new Set(['spawnSync']) }],
|
|
382
|
+
['node:fs', { defaultBinding: 'fs' }],
|
|
383
|
+
['node:path', { defaultBinding: 'path' }],
|
|
384
|
+
]);
|
|
385
|
+
function hasOverriddenTrustedSyncHelperGlobal(sourceFile) {
|
|
386
|
+
return ([...TRUSTED_SYNC_HELPER_GLOBALS].some((binding) => hasImportedBinding(sourceFile, binding)) || hasShadowedBinding(sourceFile, TRUSTED_SYNC_HELPER_GLOBALS));
|
|
387
|
+
}
|
|
388
|
+
function getAwaitedCallFromVariableStatement(statement) {
|
|
389
|
+
if (!ts.isVariableStatement(statement) ||
|
|
390
|
+
!(statement.declarationList.flags & ts.NodeFlags.Const) ||
|
|
391
|
+
statement.declarationList.declarations.length !== 1) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const declaration = statement.declarationList.declarations[0];
|
|
395
|
+
const initializer = declaration.initializer;
|
|
396
|
+
return initializer &&
|
|
397
|
+
ts.isIdentifier(declaration.name) &&
|
|
398
|
+
ts.isAwaitExpression(initializer) &&
|
|
399
|
+
ts.isCallExpression(initializer.expression)
|
|
400
|
+
? { binding: declaration.name.text, call: initializer.expression }
|
|
401
|
+
: null;
|
|
402
|
+
}
|
|
403
|
+
function hasCanonicalSyncExecutionOptions(expression, optionsBinding) {
|
|
404
|
+
if (!ts.isObjectLiteralExpression(expression)) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
const expectedProperties = ['check', 'failOnLossy', 'strict'];
|
|
408
|
+
return (expression.properties.length === expectedProperties.length &&
|
|
409
|
+
expectedProperties.every((propertyName) => expression.properties.some((property) => ts.isPropertyAssignment(property) &&
|
|
410
|
+
!ts.isComputedPropertyName(property.name) &&
|
|
411
|
+
(ts.isIdentifier(property.name) ||
|
|
412
|
+
ts.isStringLiteral(property.name)) &&
|
|
413
|
+
property.name.text === propertyName &&
|
|
414
|
+
ts.isPropertyAccessExpression(property.initializer) &&
|
|
415
|
+
ts.isIdentifier(property.initializer.expression) &&
|
|
416
|
+
property.initializer.expression.text === optionsBinding &&
|
|
417
|
+
property.initializer.name.text === propertyName)));
|
|
418
|
+
}
|
|
419
|
+
function isCanonicalSyncReportErrorGuard(statement, reportBinding) {
|
|
420
|
+
if (!ts.isIfStatement(statement) ||
|
|
421
|
+
statement.elseStatement ||
|
|
422
|
+
!ts.isBinaryExpression(statement.expression) ||
|
|
423
|
+
statement.expression.operatorToken.kind !==
|
|
424
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
|
425
|
+
!isResultPropertyAccess(statement.expression.left, reportBinding, 'status') ||
|
|
426
|
+
!ts.isStringLiteralLike(statement.expression.right) ||
|
|
427
|
+
statement.expression.right.text !== 'error' ||
|
|
428
|
+
!ts.isBlock(statement.thenStatement) ||
|
|
429
|
+
statement.thenStatement.statements.length !== 1) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
const assignment = statement.thenStatement.statements[0];
|
|
433
|
+
return (ts.isExpressionStatement(assignment) &&
|
|
434
|
+
ts.isBinaryExpression(assignment.expression) &&
|
|
435
|
+
assignment.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
436
|
+
ts.isPropertyAccessExpression(assignment.expression.left) &&
|
|
437
|
+
ts.isIdentifier(assignment.expression.left.expression) &&
|
|
438
|
+
assignment.expression.left.expression.text === 'process' &&
|
|
439
|
+
assignment.expression.left.name.text === 'exitCode' &&
|
|
440
|
+
ts.isNumericLiteral(assignment.expression.right) &&
|
|
441
|
+
assignment.expression.right.text === '1');
|
|
442
|
+
}
|
|
443
|
+
function findSyncOptionsObject(sourceFile, syncImportBindings) {
|
|
444
|
+
const main = getSingleTopLevelFunction(sourceFile, 'main');
|
|
445
|
+
if (!main?.body ||
|
|
446
|
+
!main.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ||
|
|
447
|
+
hasShadowedBinding(main, new Set(['parseCliOptions'])) ||
|
|
448
|
+
!hasCanonicalRuntimeImports(sourceFile, SYNC_TYPES_RUNTIME_IMPORTS) ||
|
|
449
|
+
!hasCanonicalCheckParser(sourceFile, [
|
|
450
|
+
{ flagName: '--strict', propertyName: 'strict' },
|
|
451
|
+
{ flagName: '--fail-on-lossy', propertyName: 'failOnLossy' },
|
|
452
|
+
], true) ||
|
|
453
|
+
!hasTopLevelMainInvocation(sourceFile, new Set(['@wp-typia/block-runtime/metadata-core']))) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
const statements = [...main.body.statements];
|
|
457
|
+
const optionsDeclaration = getDirectVariableBinding(statements, isParseCliOptionsCall);
|
|
458
|
+
if (!optionsDeclaration) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
const calls = statements.flatMap((statement, index) => {
|
|
462
|
+
const awaitedCall = getAwaitedCallFromVariableStatement(statement);
|
|
463
|
+
const call = awaitedCall?.call;
|
|
464
|
+
if (!call ||
|
|
465
|
+
!ts.isIdentifier(call.expression) ||
|
|
466
|
+
!syncImportBindings.has(call.expression.text) ||
|
|
467
|
+
call.arguments.length !== 2 ||
|
|
468
|
+
!ts.isObjectLiteralExpression(call.arguments[0]) ||
|
|
469
|
+
!hasCanonicalSyncExecutionOptions(call.arguments[1], optionsDeclaration.binding)) {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
return [
|
|
473
|
+
{
|
|
474
|
+
index,
|
|
475
|
+
options: call.arguments[0],
|
|
476
|
+
reportBinding: awaitedCall.binding,
|
|
477
|
+
},
|
|
478
|
+
];
|
|
479
|
+
});
|
|
480
|
+
if (calls.length !== 1) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
const [call] = calls;
|
|
484
|
+
if (call.index !== optionsDeclaration.index + 1 ||
|
|
485
|
+
hasEarlierAbruptCompletion(statements, call.index)) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
const errorGuardIndexes = statements.flatMap((statement, index) => index > call.index &&
|
|
489
|
+
isCanonicalSyncReportErrorGuard(statement, call.reportBinding)
|
|
490
|
+
? [index]
|
|
491
|
+
: []);
|
|
492
|
+
const errorGuardIndex = errorGuardIndexes[0];
|
|
493
|
+
return errorGuardIndexes.length === 1 &&
|
|
494
|
+
errorGuardIndex === statements.length - 1 &&
|
|
495
|
+
hasCanonicalSyncReportRendering(sourceFile, statements.slice(call.index + 1, errorGuardIndex), optionsDeclaration.binding, call.reportBinding) &&
|
|
496
|
+
!hasEarlierAbruptCompletion(statements.slice(call.index + 1), errorGuardIndex - call.index - 1)
|
|
497
|
+
? call.options
|
|
498
|
+
: null;
|
|
499
|
+
}
|
|
500
|
+
function parseStandaloneSyncConfig(project) {
|
|
501
|
+
const syncScriptPath = path.join(project.projectDir, STANDALONE_SYNC_SCRIPT);
|
|
502
|
+
let source;
|
|
503
|
+
try {
|
|
504
|
+
source = fs.readFileSync(syncScriptPath, 'utf8');
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return {
|
|
508
|
+
options: null,
|
|
509
|
+
problem: `Unable to read generated helper ${STANDALONE_SYNC_SCRIPT}`,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const sourceFile = ts.createSourceFile(syncScriptPath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
513
|
+
if (hasTypeScriptSyntaxErrors(source, syncScriptPath)) {
|
|
514
|
+
return {
|
|
515
|
+
options: null,
|
|
516
|
+
problem: `${STANDALONE_SYNC_SCRIPT} contains TypeScript syntax errors.`,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const syncImportBindings = getCanonicalSyncImportBindings(sourceFile);
|
|
520
|
+
if (hasShadowedBinding(sourceFile, syncImportBindings)) {
|
|
521
|
+
return {
|
|
522
|
+
options: null,
|
|
523
|
+
problem: `${STANDALONE_SYNC_SCRIPT} must not shadow its canonical ` +
|
|
524
|
+
'runSyncBlockMetadata() import binding.',
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const optionsObject = findSyncOptionsObject(sourceFile, syncImportBindings);
|
|
528
|
+
if (!optionsObject) {
|
|
529
|
+
return {
|
|
530
|
+
options: null,
|
|
531
|
+
problem: `${STANDALONE_SYNC_SCRIPT} must import and call ` +
|
|
532
|
+
'runSyncBlockMetadata() from @wp-typia/block-runtime/metadata-core with a static options object.',
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (hasUnsupportedSyncOptionSyntax(optionsObject)) {
|
|
536
|
+
return {
|
|
537
|
+
options: null,
|
|
538
|
+
problem: `${STANDALONE_SYNC_SCRIPT} must use direct static property ` +
|
|
539
|
+
'assignments; spread, shorthand, and computed properties are not supported.',
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
if (optionsObject.properties.some((property) => ts.isPropertyAssignment(property) &&
|
|
543
|
+
!ts.isComputedPropertyName(property.name) &&
|
|
544
|
+
(ts.isIdentifier(property.name) ||
|
|
545
|
+
ts.isStringLiteral(property.name)) &&
|
|
546
|
+
property.name.text === 'projectRoot')) {
|
|
547
|
+
return {
|
|
548
|
+
options: null,
|
|
549
|
+
problem: `${STANDALONE_SYNC_SCRIPT} must not override projectRoot; ` +
|
|
550
|
+
'standalone sync helpers must run from their package root.',
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const blockJsonFile = getObjectPropertyString(optionsObject, 'blockJsonFile');
|
|
554
|
+
const sourceTypeName = getObjectPropertyString(optionsObject, 'sourceTypeName');
|
|
555
|
+
const typesFile = getObjectPropertyString(optionsObject, 'typesFile');
|
|
556
|
+
if (!blockJsonFile || !sourceTypeName || !typesFile) {
|
|
557
|
+
return {
|
|
558
|
+
options: null,
|
|
559
|
+
problem: `${STANDALONE_SYNC_SCRIPT} must define static blockJsonFile, ` +
|
|
560
|
+
'sourceTypeName, and typesFile values.',
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const optionalArtifactPathNames = [
|
|
564
|
+
'jsonSchemaFile',
|
|
565
|
+
'manifestFile',
|
|
566
|
+
'openApiFile',
|
|
567
|
+
'phpValidatorFile',
|
|
568
|
+
];
|
|
569
|
+
const nonLiteralOptionalPath = optionalArtifactPathNames.find((propertyName) => {
|
|
570
|
+
const property = getObjectPropertyAssignment(optionsObject, propertyName);
|
|
571
|
+
return property && !ts.isStringLiteralLike(property.initializer);
|
|
572
|
+
});
|
|
573
|
+
if (nonLiteralOptionalPath) {
|
|
574
|
+
return {
|
|
575
|
+
options: null,
|
|
576
|
+
problem: `${STANDALONE_SYNC_SCRIPT} must define optional artifact path ` +
|
|
577
|
+
`${nonLiteralOptionalPath} as a static string value.`,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
const optionalPaths = {
|
|
581
|
+
jsonSchemaFile: getObjectPropertyString(optionsObject, 'jsonSchemaFile'),
|
|
582
|
+
manifestFile: getObjectPropertyString(optionsObject, 'manifestFile'),
|
|
583
|
+
openApiFile: getObjectPropertyString(optionsObject, 'openApiFile'),
|
|
584
|
+
phpValidatorFile: getObjectPropertyString(optionsObject, 'phpValidatorFile'),
|
|
585
|
+
};
|
|
586
|
+
const configuredPaths = [
|
|
587
|
+
blockJsonFile,
|
|
588
|
+
typesFile,
|
|
589
|
+
...Object.values(optionalPaths).filter((filePath) => typeof filePath === 'string'),
|
|
590
|
+
];
|
|
591
|
+
const unsafePath = configuredPaths.find((filePath) => !isSafeProjectRelativePath(project.projectDir, filePath));
|
|
592
|
+
if (unsafePath) {
|
|
593
|
+
return {
|
|
594
|
+
options: null,
|
|
595
|
+
problem: `${STANDALONE_SYNC_SCRIPT} references a path outside the project root: ${unsafePath}`,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
if (blockJsonFile !== STANDALONE_BLOCK_JSON_FILE ||
|
|
599
|
+
optionalPaths.manifestFile !== STANDALONE_MANIFEST_FILE) {
|
|
600
|
+
return {
|
|
601
|
+
options: null,
|
|
602
|
+
problem: `${STANDALONE_SYNC_SCRIPT} must use canonical ` +
|
|
603
|
+
`${STANDALONE_BLOCK_JSON_FILE} and ${STANDALONE_MANIFEST_FILE} artifact paths.`,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
options: {
|
|
608
|
+
blockJsonFile,
|
|
609
|
+
...(optionalPaths.jsonSchemaFile
|
|
610
|
+
? { jsonSchemaFile: optionalPaths.jsonSchemaFile }
|
|
611
|
+
: {}),
|
|
612
|
+
...(optionalPaths.manifestFile
|
|
613
|
+
? { manifestFile: optionalPaths.manifestFile }
|
|
614
|
+
: {}),
|
|
615
|
+
...(optionalPaths.openApiFile
|
|
616
|
+
? { openApiFile: optionalPaths.openApiFile }
|
|
617
|
+
: {}),
|
|
618
|
+
...(optionalPaths.phpValidatorFile
|
|
619
|
+
? { phpValidatorFile: optionalPaths.phpValidatorFile }
|
|
620
|
+
: {}),
|
|
621
|
+
projectRoot: project.projectDir,
|
|
622
|
+
sourceTypeName,
|
|
623
|
+
typesFile,
|
|
624
|
+
},
|
|
625
|
+
problem: null,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function isSyncScriptPathExpression(expression, expectedScriptPath) {
|
|
629
|
+
const pathParts = expectedScriptPath.split(path.sep);
|
|
630
|
+
if (ts.isStringLiteralLike(expression)) {
|
|
631
|
+
return expression.text === pathParts.join('/');
|
|
632
|
+
}
|
|
633
|
+
return (ts.isCallExpression(expression) &&
|
|
634
|
+
ts.isPropertyAccessExpression(expression.expression) &&
|
|
635
|
+
ts.isIdentifier(expression.expression.expression) &&
|
|
636
|
+
expression.expression.expression.text === 'path' &&
|
|
637
|
+
expression.expression.name.text === 'join' &&
|
|
638
|
+
expression.arguments.length === pathParts.length &&
|
|
639
|
+
expression.arguments.every((argument, index) => ts.isStringLiteralLike(argument) &&
|
|
640
|
+
argument.text === pathParts[index]));
|
|
641
|
+
}
|
|
642
|
+
function getNamedImportBindingsFromModule(sourceFile, moduleName, importedName) {
|
|
643
|
+
const bindings = new Set();
|
|
644
|
+
for (const statement of sourceFile.statements) {
|
|
645
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
646
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
647
|
+
statement.moduleSpecifier.text !== moduleName ||
|
|
648
|
+
statement.importClause?.isTypeOnly ||
|
|
649
|
+
!statement.importClause?.namedBindings ||
|
|
650
|
+
!ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
654
|
+
if (!element.isTypeOnly &&
|
|
655
|
+
(element.propertyName ?? element.name).text === importedName) {
|
|
656
|
+
bindings.add(element.name.text);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return bindings;
|
|
661
|
+
}
|
|
662
|
+
function hasCanonicalDefaultImport(sourceFile, moduleName, localName) {
|
|
663
|
+
return sourceFile.statements.some((statement) => ts.isImportDeclaration(statement) &&
|
|
664
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
665
|
+
statement.moduleSpecifier.text === moduleName &&
|
|
666
|
+
!statement.importClause?.isTypeOnly &&
|
|
667
|
+
statement.importClause?.name?.text === localName);
|
|
668
|
+
}
|
|
669
|
+
function getSingleTopLevelFunction(sourceFile, functionName) {
|
|
670
|
+
const declarations = sourceFile.statements.filter((statement) => ts.isFunctionDeclaration(statement) &&
|
|
671
|
+
statement.name?.text === functionName &&
|
|
672
|
+
statement.body !== undefined);
|
|
673
|
+
return declarations.length === 1 ? declarations[0] : null;
|
|
674
|
+
}
|
|
675
|
+
function getDirectCall(statement) {
|
|
676
|
+
return ts.isExpressionStatement(statement) &&
|
|
677
|
+
ts.isCallExpression(statement.expression)
|
|
678
|
+
? statement.expression
|
|
679
|
+
: null;
|
|
680
|
+
}
|
|
681
|
+
function isDirectMethodCall(statement, objectBinding, methodName, argument) {
|
|
682
|
+
const call = getDirectCall(statement);
|
|
683
|
+
return (call !== null &&
|
|
684
|
+
ts.isPropertyAccessExpression(call.expression) &&
|
|
685
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
686
|
+
call.expression.expression.text === objectBinding &&
|
|
687
|
+
call.expression.name.text === methodName &&
|
|
688
|
+
call.arguments.length === 1 &&
|
|
689
|
+
ts.isStringLiteralLike(call.arguments[0]) &&
|
|
690
|
+
call.arguments[0].text === argument);
|
|
691
|
+
}
|
|
692
|
+
function isDirectRunSyncCall(statement, scriptPathBinding, optionsBinding) {
|
|
693
|
+
const call = getDirectCall(statement);
|
|
694
|
+
return (call !== null &&
|
|
695
|
+
ts.isIdentifier(call.expression) &&
|
|
696
|
+
call.expression.text === 'runSyncScript' &&
|
|
697
|
+
call.arguments.length === 2 &&
|
|
698
|
+
ts.isIdentifier(call.arguments[0]) &&
|
|
699
|
+
call.arguments[0].text === scriptPathBinding &&
|
|
700
|
+
ts.isIdentifier(call.arguments[1]) &&
|
|
701
|
+
call.arguments[1].text === optionsBinding);
|
|
702
|
+
}
|
|
703
|
+
function isNonTargetArgumentGuard(node, argumentBinding, targetArgument) {
|
|
704
|
+
if (!ts.isIfStatement(node) ||
|
|
705
|
+
node.elseStatement ||
|
|
706
|
+
!ts.isBinaryExpression(node.expression) ||
|
|
707
|
+
node.expression.operatorToken.kind !==
|
|
708
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
const { left, right } = node.expression;
|
|
712
|
+
return (((ts.isIdentifier(left) &&
|
|
713
|
+
left.text === argumentBinding &&
|
|
714
|
+
ts.isStringLiteralLike(right) &&
|
|
715
|
+
right.text !== targetArgument) ||
|
|
716
|
+
(ts.isStringLiteralLike(left) &&
|
|
717
|
+
left.text !== targetArgument &&
|
|
718
|
+
ts.isIdentifier(right) &&
|
|
719
|
+
right.text === argumentBinding)));
|
|
720
|
+
}
|
|
721
|
+
function isLoopStatement(node) {
|
|
722
|
+
return (ts.isDoStatement(node) ||
|
|
723
|
+
ts.isForInStatement(node) ||
|
|
724
|
+
ts.isForOfStatement(node) ||
|
|
725
|
+
ts.isForStatement(node) ||
|
|
726
|
+
ts.isWhileStatement(node));
|
|
727
|
+
}
|
|
728
|
+
function labelsLoop(statement) {
|
|
729
|
+
let target = statement;
|
|
730
|
+
while (ts.isLabeledStatement(target)) {
|
|
731
|
+
target = target.statement;
|
|
732
|
+
}
|
|
733
|
+
return isLoopStatement(target);
|
|
734
|
+
}
|
|
735
|
+
function containsParserControlFlow(node, argumentBinding, check, breakableDepth = 0, loopDepth = 0, outerContinueIsSafe = false, activeLabels = new Map(), targetArgument = '--check') {
|
|
736
|
+
if (ts.isFunctionLike(node)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
if (check === 'outer-break-or-return') {
|
|
740
|
+
if (ts.isReturnStatement(node)) {
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
if (ts.isBreakStatement(node)) {
|
|
744
|
+
return node.label
|
|
745
|
+
? !activeLabels.has(node.label.text)
|
|
746
|
+
: breakableDepth === 0;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else if (ts.isContinueStatement(node)) {
|
|
750
|
+
return node.label
|
|
751
|
+
? activeLabels.get(node.label.text) !== true
|
|
752
|
+
: loopDepth === 0 && !outerContinueIsSafe;
|
|
753
|
+
}
|
|
754
|
+
if (check === 'unsafe-continue' &&
|
|
755
|
+
isNonTargetArgumentGuard(node, argumentBinding, targetArgument)) {
|
|
756
|
+
return containsParserControlFlow(node.thenStatement, argumentBinding, check, breakableDepth, loopDepth, true, activeLabels, targetArgument);
|
|
757
|
+
}
|
|
758
|
+
if (ts.isLabeledStatement(node)) {
|
|
759
|
+
const nestedLabels = new Map(activeLabels);
|
|
760
|
+
nestedLabels.set(node.label.text, labelsLoop(node.statement));
|
|
761
|
+
return containsParserControlFlow(node.statement, argumentBinding, check, breakableDepth, loopDepth, outerContinueIsSafe, nestedLabels, targetArgument);
|
|
762
|
+
}
|
|
763
|
+
const nextBreakableDepth = breakableDepth +
|
|
764
|
+
(isLoopStatement(node) || ts.isSwitchStatement(node) ? 1 : 0);
|
|
765
|
+
const nextLoopDepth = loopDepth + (isLoopStatement(node) ? 1 : 0);
|
|
766
|
+
let found = false;
|
|
767
|
+
ts.forEachChild(node, (child) => {
|
|
768
|
+
if (!found &&
|
|
769
|
+
containsParserControlFlow(child, argumentBinding, check, nextBreakableDepth, nextLoopDepth, outerContinueIsSafe, activeLabels, targetArgument)) {
|
|
770
|
+
found = true;
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
return found;
|
|
774
|
+
}
|
|
775
|
+
function isResultPropertyAccess(expression, resultBinding, propertyName) {
|
|
776
|
+
return (expression !== undefined &&
|
|
777
|
+
ts.isPropertyAccessExpression(expression) &&
|
|
778
|
+
ts.isIdentifier(expression.expression) &&
|
|
779
|
+
expression.expression.text === resultBinding &&
|
|
780
|
+
expression.name.text === propertyName);
|
|
781
|
+
}
|
|
782
|
+
function isCanonicalStaticErrorThrow(statement) {
|
|
783
|
+
if (!ts.isThrowStatement(statement) || !statement.expression) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
const errorConstruction = getSafeErrorConstruction(statement.expression);
|
|
787
|
+
return (errorConstruction !== null &&
|
|
788
|
+
ts.isStringLiteralLike(errorConstruction.arguments[0]));
|
|
789
|
+
}
|
|
790
|
+
function isDirectFileNotFoundCondition(expression, resultBinding) {
|
|
791
|
+
if (!ts.isBinaryExpression(expression) ||
|
|
792
|
+
expression.operatorToken.kind !== ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
|
793
|
+
!ts.isStringLiteralLike(expression.right) ||
|
|
794
|
+
expression.right.text !== 'ENOENT' ||
|
|
795
|
+
!ts.isPropertyAccessExpression(expression.left) ||
|
|
796
|
+
expression.left.name.text !== 'code') {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
return isResultPropertyAccess(unwrapStaticExpression(expression.left.expression), resultBinding, 'error');
|
|
800
|
+
}
|
|
801
|
+
function isFileNotFoundHelperCondition(expression, resultBinding) {
|
|
802
|
+
return (ts.isCallExpression(expression) &&
|
|
803
|
+
ts.isIdentifier(expression.expression) &&
|
|
804
|
+
expression.expression.text === 'isFileNotFoundError' &&
|
|
805
|
+
expression.arguments.length === 1 &&
|
|
806
|
+
isResultPropertyAccess(expression.arguments[0], resultBinding, 'error'));
|
|
807
|
+
}
|
|
808
|
+
function hasCanonicalRunnerErrorGuard(sourceFile, statement, resultBinding) {
|
|
809
|
+
if (!statement ||
|
|
810
|
+
!ts.isIfStatement(statement) ||
|
|
811
|
+
statement.elseStatement ||
|
|
812
|
+
!isResultPropertyAccess(statement.expression, resultBinding, 'error') ||
|
|
813
|
+
!ts.isBlock(statement.thenStatement) ||
|
|
814
|
+
statement.thenStatement.statements.length !== 2) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
const [fileNotFoundGuard, finalStatement] = statement.thenStatement.statements;
|
|
818
|
+
if (!ts.isIfStatement(fileNotFoundGuard) ||
|
|
819
|
+
fileNotFoundGuard.elseStatement ||
|
|
820
|
+
!ts.isBlock(fileNotFoundGuard.thenStatement) ||
|
|
821
|
+
fileNotFoundGuard.thenStatement.statements.length !== 1 ||
|
|
822
|
+
!isCanonicalStaticErrorThrow(fileNotFoundGuard.thenStatement.statements[0])) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
const hasDirectCondition = isDirectFileNotFoundCondition(fileNotFoundGuard.expression, resultBinding);
|
|
826
|
+
const hasHelperCondition = isFileNotFoundHelperCondition(fileNotFoundGuard.expression, resultBinding);
|
|
827
|
+
return ((hasDirectCondition ||
|
|
828
|
+
(hasHelperCondition &&
|
|
829
|
+
hasCanonicalFileNotFoundErrorHelpers(sourceFile))) &&
|
|
830
|
+
ts.isThrowStatement(finalStatement) &&
|
|
831
|
+
isResultPropertyAccess(finalStatement.expression, resultBinding, 'error'));
|
|
832
|
+
}
|
|
833
|
+
function hasCanonicalRunnerStatusGuard(statement, resultBinding, scriptPathBinding) {
|
|
834
|
+
if (!statement ||
|
|
835
|
+
!ts.isIfStatement(statement) ||
|
|
836
|
+
statement.elseStatement ||
|
|
837
|
+
!ts.isBinaryExpression(statement.expression) ||
|
|
838
|
+
statement.expression.operatorToken.kind !==
|
|
839
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
840
|
+
!isResultPropertyAccess(statement.expression.left, resultBinding, 'status') ||
|
|
841
|
+
!ts.isNumericLiteral(statement.expression.right) ||
|
|
842
|
+
statement.expression.right.text !== '0' ||
|
|
843
|
+
!ts.isBlock(statement.thenStatement) ||
|
|
844
|
+
statement.thenStatement.statements.length !== 1) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
const [finalStatement] = statement.thenStatement.statements;
|
|
848
|
+
if (!ts.isThrowStatement(finalStatement) || !finalStatement.expression) {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
const errorConstruction = getSafeErrorConstruction(finalStatement.expression);
|
|
852
|
+
if (errorConstruction === null)
|
|
853
|
+
return false;
|
|
854
|
+
const message = errorConstruction.arguments[0];
|
|
855
|
+
return (ts.isStringLiteralLike(message) ||
|
|
856
|
+
(ts.isTemplateExpression(message) &&
|
|
857
|
+
message.templateSpans.length === 1 &&
|
|
858
|
+
ts.isIdentifier(message.templateSpans[0].expression) &&
|
|
859
|
+
message.templateSpans[0].expression.text === scriptPathBinding));
|
|
860
|
+
}
|
|
861
|
+
function getTypeScriptNodeFingerprint(node) {
|
|
862
|
+
const parts = [];
|
|
863
|
+
function visit(candidate) {
|
|
864
|
+
parts.push(`node:${candidate.kind}`);
|
|
865
|
+
if (ts.isIdentifier(candidate) ||
|
|
866
|
+
ts.isStringLiteralLike(candidate) ||
|
|
867
|
+
ts.isNumericLiteral(candidate) ||
|
|
868
|
+
ts.isTemplateLiteralToken(candidate)) {
|
|
869
|
+
parts.push(`text:${candidate.text}`);
|
|
870
|
+
}
|
|
871
|
+
ts.forEachChild(candidate, visit);
|
|
872
|
+
parts.push('end');
|
|
873
|
+
}
|
|
874
|
+
visit(node);
|
|
875
|
+
return JSON.stringify(parts);
|
|
876
|
+
}
|
|
877
|
+
const CANONICAL_FILE_NOT_FOUND_HELPERS_SOURCE = `
|
|
878
|
+
function getOptionalNodeErrorCode(error: unknown): string | undefined {
|
|
879
|
+
return typeof error === 'object' && error !== null && 'code' in error
|
|
880
|
+
? String((error as { code: unknown }).code)
|
|
881
|
+
: undefined;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function isFileNotFoundError(error: unknown): boolean {
|
|
885
|
+
return getOptionalNodeErrorCode(error) === 'ENOENT';
|
|
886
|
+
}
|
|
887
|
+
`;
|
|
888
|
+
const CANONICAL_FILE_NOT_FOUND_HELPER_FINGERPRINTS = ts
|
|
889
|
+
.createSourceFile('canonical-file-not-found-helpers.ts', CANONICAL_FILE_NOT_FOUND_HELPERS_SOURCE, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
890
|
+
.statements.map(getTypeScriptNodeFingerprint);
|
|
891
|
+
function hasCanonicalFileNotFoundErrorHelpers(sourceFile) {
|
|
892
|
+
const getErrorCode = getSingleTopLevelFunction(sourceFile, 'getOptionalNodeErrorCode');
|
|
893
|
+
const isFileNotFound = getSingleTopLevelFunction(sourceFile, 'isFileNotFoundError');
|
|
894
|
+
return (getErrorCode !== null &&
|
|
895
|
+
isFileNotFound !== null &&
|
|
896
|
+
!hasImportedBinding(sourceFile, 'getOptionalNodeErrorCode') &&
|
|
897
|
+
!hasImportedBinding(sourceFile, 'isFileNotFoundError') &&
|
|
898
|
+
!hasImportedBinding(sourceFile, 'String') &&
|
|
899
|
+
!hasShadowedBinding(sourceFile, new Set(['String'])) &&
|
|
900
|
+
getTypeScriptNodeFingerprint(getErrorCode) ===
|
|
901
|
+
CANONICAL_FILE_NOT_FOUND_HELPER_FINGERPRINTS[0] &&
|
|
902
|
+
getTypeScriptNodeFingerprint(isFileNotFound) ===
|
|
903
|
+
CANONICAL_FILE_NOT_FOUND_HELPER_FINGERPRINTS[1]);
|
|
904
|
+
}
|
|
905
|
+
const CANONICAL_SYNC_REPORT_HELPER_SOURCE = `
|
|
906
|
+
function printHumanReport(
|
|
907
|
+
options: SyncTypesCliOptions,
|
|
908
|
+
report: Awaited<ReturnType<typeof runSyncBlockMetadata>>,
|
|
909
|
+
) {
|
|
910
|
+
if (report.failure) {
|
|
911
|
+
console.error("❌ Type sync failed:", report.failure.message);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
console.log(
|
|
915
|
+
options.check
|
|
916
|
+
? "✅ block.json, typia.manifest.json, and typia-validator.php are already up to date with the TypeScript types!"
|
|
917
|
+
: "✅ block.json, typia.manifest.json, and typia-validator.php were generated from TypeScript types!",
|
|
918
|
+
);
|
|
919
|
+
console.log("📝 Generated attributes:", report.attributeNames);
|
|
920
|
+
if (report.lossyProjectionWarnings.length > 0) {
|
|
921
|
+
console.warn("⚠️ Some Typia constraints were preserved only in typia.manifest.json:");
|
|
922
|
+
for (const warning of report.lossyProjectionWarnings) {
|
|
923
|
+
console.warn(\` - \${warning}\`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (report.phpGenerationWarnings.length > 0) {
|
|
927
|
+
console.warn("⚠️ Some Typia constraints are not yet enforced by typia-validator.php:");
|
|
928
|
+
for (const warning of report.phpGenerationWarnings) {
|
|
929
|
+
console.warn(\` - \${warning}\`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (report.status === 'error') {
|
|
933
|
+
console.error(
|
|
934
|
+
"❌ Type sync completed with warnings treated as errors because of the selected flags.",
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
`;
|
|
939
|
+
const CANONICAL_PERSISTENCE_SYNC_REPORT_HELPER_SOURCE = CANONICAL_SYNC_REPORT_HELPER_SOURCE.replace('✅ block.json, typia.manifest.json, and typia-validator.php are already up to date with the TypeScript types!', '✅ block.json, typia.manifest.json, typia-validator.php, typia.schema.json, and typia.openapi.json are already up to date with the TypeScript types!').replace('✅ block.json, typia.manifest.json, and typia-validator.php were generated from TypeScript types!', '✅ block.json, typia.manifest.json, typia-validator.php, typia.schema.json, and typia.openapi.json were generated from TypeScript types!');
|
|
940
|
+
const CANONICAL_SYNC_REPORT_HELPER_FINGERPRINTS = new Set([
|
|
941
|
+
CANONICAL_SYNC_REPORT_HELPER_SOURCE,
|
|
942
|
+
CANONICAL_PERSISTENCE_SYNC_REPORT_HELPER_SOURCE,
|
|
943
|
+
].map((source) => getTypeScriptNodeFingerprint(ts.createSourceFile('canonical-sync-report-helper.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS).statements[0])));
|
|
944
|
+
function hasCanonicalSyncReportRendering(sourceFile, statements, optionsBinding, reportBinding) {
|
|
945
|
+
if (statements.length === 0) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
const helper = getSingleTopLevelFunction(sourceFile, 'printHumanReport');
|
|
949
|
+
const reportGlobals = new Set(['console', 'JSON']);
|
|
950
|
+
if (statements.length !== 1 ||
|
|
951
|
+
helper === null ||
|
|
952
|
+
hasImportedBinding(sourceFile, 'printHumanReport') ||
|
|
953
|
+
[...reportGlobals].some((binding) => hasImportedBinding(sourceFile, binding)) ||
|
|
954
|
+
hasShadowedBinding(sourceFile, reportGlobals) ||
|
|
955
|
+
hasShadowedBinding(getSingleTopLevelFunction(sourceFile, 'main') ?? sourceFile, new Set(['printHumanReport'])) ||
|
|
956
|
+
!CANONICAL_SYNC_REPORT_HELPER_FINGERPRINTS.has(getTypeScriptNodeFingerprint(helper))) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
const expectedSource = `
|
|
960
|
+
if (${optionsBinding}.report === 'json') {
|
|
961
|
+
process.stdout.write(\`\${JSON.stringify(${reportBinding}, null, 2)}\\n\`);
|
|
962
|
+
} else {
|
|
963
|
+
printHumanReport(${optionsBinding}, ${reportBinding});
|
|
964
|
+
}
|
|
965
|
+
`;
|
|
966
|
+
const expectedStatement = ts.createSourceFile('canonical-sync-report-rendering.ts', expectedSource, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS).statements[0];
|
|
967
|
+
return (expectedStatement !== undefined &&
|
|
968
|
+
getTypeScriptNodeFingerprint(statements[0]) ===
|
|
969
|
+
getTypeScriptNodeFingerprint(expectedStatement));
|
|
970
|
+
}
|
|
971
|
+
// Compare syntax trees instead of source text so formatting, comments, quote
|
|
972
|
+
// style, and optional semicolons do not affect the generated-helper contract.
|
|
973
|
+
const CANONICAL_SYNC_SCRIPT_ENV_HELPER_SOURCE = `
|
|
974
|
+
function getSyncScriptEnv() {
|
|
975
|
+
const binaryDirectory = path.join(process.cwd(), 'node_modules', '.bin');
|
|
976
|
+
const inheritedPath = process.env.PATH ?? process.env.Path ?? Object.entries(process.env).find(([key]) => key.toLowerCase() === 'path')?.[1] ?? '';
|
|
977
|
+
const nextPath = fs.existsSync(binaryDirectory) ? \`\${binaryDirectory}\${path.delimiter}\${inheritedPath}\` : inheritedPath;
|
|
978
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
979
|
+
for (const key of Object.keys(env)) {
|
|
980
|
+
if (key.toLowerCase() === 'path') {
|
|
981
|
+
delete env[key];
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
env.PATH = nextPath;
|
|
985
|
+
return env;
|
|
986
|
+
}
|
|
987
|
+
`;
|
|
988
|
+
const CANONICAL_SYNC_SCRIPT_ENV_HELPER_FINGERPRINT = getTypeScriptNodeFingerprint(ts.createSourceFile('canonical-sync-project-env.ts', CANONICAL_SYNC_SCRIPT_ENV_HELPER_SOURCE, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS).statements[0]);
|
|
989
|
+
function hasCanonicalSyncScriptEnv(sourceFile) {
|
|
990
|
+
const helper = getSingleTopLevelFunction(sourceFile, 'getSyncScriptEnv');
|
|
991
|
+
return (helper !== null &&
|
|
992
|
+
!hasImportedBinding(sourceFile, 'getSyncScriptEnv') &&
|
|
993
|
+
hasCanonicalDefaultImport(sourceFile, 'node:fs', 'fs') &&
|
|
994
|
+
hasCanonicalDefaultImport(sourceFile, 'node:path', 'path') &&
|
|
995
|
+
getTypeScriptNodeFingerprint(helper) ===
|
|
996
|
+
CANONICAL_SYNC_SCRIPT_ENV_HELPER_FINGERPRINT);
|
|
997
|
+
}
|
|
998
|
+
function hasCanonicalSpawnOptions(expression) {
|
|
999
|
+
const properties = new Map();
|
|
1000
|
+
for (const property of expression.properties) {
|
|
1001
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
1002
|
+
ts.isComputedPropertyName(property.name) ||
|
|
1003
|
+
(!ts.isIdentifier(property.name) &&
|
|
1004
|
+
!ts.isStringLiteral(property.name)) ||
|
|
1005
|
+
properties.has(property.name.text)) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
properties.set(property.name.text, property.initializer);
|
|
1009
|
+
}
|
|
1010
|
+
if (properties.size !== 4 ||
|
|
1011
|
+
!['cwd', 'env', 'shell', 'stdio'].every((propertyName) => properties.has(propertyName))) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
const cwd = properties.get('cwd');
|
|
1015
|
+
const env = properties.get('env');
|
|
1016
|
+
const shell = properties.get('shell');
|
|
1017
|
+
const stdio = properties.get('stdio');
|
|
1018
|
+
return (cwd !== undefined &&
|
|
1019
|
+
ts.isCallExpression(cwd) &&
|
|
1020
|
+
ts.isPropertyAccessExpression(cwd.expression) &&
|
|
1021
|
+
ts.isIdentifier(cwd.expression.expression) &&
|
|
1022
|
+
cwd.expression.expression.text === 'process' &&
|
|
1023
|
+
cwd.expression.name.text === 'cwd' &&
|
|
1024
|
+
cwd.arguments.length === 0 &&
|
|
1025
|
+
env !== undefined &&
|
|
1026
|
+
ts.isCallExpression(env) &&
|
|
1027
|
+
ts.isIdentifier(env.expression) &&
|
|
1028
|
+
env.expression.text === 'getSyncScriptEnv' &&
|
|
1029
|
+
env.arguments.length === 0 &&
|
|
1030
|
+
shell !== undefined &&
|
|
1031
|
+
ts.isBinaryExpression(shell) &&
|
|
1032
|
+
shell.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
|
|
1033
|
+
ts.isPropertyAccessExpression(shell.left) &&
|
|
1034
|
+
ts.isIdentifier(shell.left.expression) &&
|
|
1035
|
+
shell.left.expression.text === 'process' &&
|
|
1036
|
+
shell.left.name.text === 'platform' &&
|
|
1037
|
+
ts.isStringLiteralLike(shell.right) &&
|
|
1038
|
+
shell.right.text === 'win32' &&
|
|
1039
|
+
stdio !== undefined &&
|
|
1040
|
+
ts.isStringLiteralLike(stdio) &&
|
|
1041
|
+
stdio.text === 'inherit');
|
|
1042
|
+
}
|
|
1043
|
+
function hasCanonicalSyncRunner(sourceFile) {
|
|
1044
|
+
const runner = getSingleTopLevelFunction(sourceFile, 'runSyncScript');
|
|
1045
|
+
if (!runner?.body ||
|
|
1046
|
+
runner.parameters.length !== 2 ||
|
|
1047
|
+
runner.asteriskToken !== undefined ||
|
|
1048
|
+
runner.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ||
|
|
1049
|
+
!hasCanonicalSyncScriptEnv(sourceFile) ||
|
|
1050
|
+
!hasCanonicalRuntimeImports(sourceFile, SYNC_PROJECT_RUNTIME_IMPORTS)) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
const [scriptPathParameter, optionsParameter] = runner.parameters;
|
|
1054
|
+
if (!ts.isIdentifier(scriptPathParameter.name) ||
|
|
1055
|
+
!ts.isIdentifier(optionsParameter.name) ||
|
|
1056
|
+
scriptPathParameter.dotDotDotToken !== undefined ||
|
|
1057
|
+
scriptPathParameter.initializer !== undefined ||
|
|
1058
|
+
scriptPathParameter.questionToken !== undefined ||
|
|
1059
|
+
optionsParameter.dotDotDotToken !== undefined ||
|
|
1060
|
+
optionsParameter.initializer !== undefined ||
|
|
1061
|
+
optionsParameter.questionToken !== undefined) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
const scriptPathParameterName = scriptPathParameter.name.text;
|
|
1065
|
+
const optionsParameterName = optionsParameter.name.text;
|
|
1066
|
+
const spawnBindings = getNamedImportBindingsFromModule(sourceFile, 'node:child_process', 'spawnSync');
|
|
1067
|
+
if (spawnBindings.size === 0 ||
|
|
1068
|
+
hasShadowedBinding(sourceFile, spawnBindings) ||
|
|
1069
|
+
hasShadowedBinding(runner, new Set([
|
|
1070
|
+
'getOptionalNodeErrorCode',
|
|
1071
|
+
'getSyncScriptEnv',
|
|
1072
|
+
'isFileNotFoundError',
|
|
1073
|
+
]))) {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
const statements = [...runner.body.statements];
|
|
1077
|
+
if (statements.length !== 5) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
const argsDeclarations = [];
|
|
1081
|
+
statements.forEach((statement, index) => {
|
|
1082
|
+
if (!ts.isVariableStatement(statement) ||
|
|
1083
|
+
!(statement.declarationList.flags & ts.NodeFlags.Const) ||
|
|
1084
|
+
statement.declarationList.declarations.length !== 1) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const declaration = statement.declarationList.declarations[0];
|
|
1088
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
1089
|
+
declaration.initializer &&
|
|
1090
|
+
ts.isArrayLiteralExpression(declaration.initializer) &&
|
|
1091
|
+
declaration.initializer.elements.length === 1 &&
|
|
1092
|
+
ts.isIdentifier(declaration.initializer.elements[0]) &&
|
|
1093
|
+
declaration.initializer.elements[0].text === scriptPathParameterName) {
|
|
1094
|
+
argsDeclarations.push({ binding: declaration.name.text, index });
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
if (argsDeclarations.length !== 1) {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
const [{ binding: argsBinding, index: argsIndex }] = argsDeclarations;
|
|
1101
|
+
const checkGuardIndexes = statements.flatMap((statement, index) => {
|
|
1102
|
+
if (!ts.isIfStatement(statement) ||
|
|
1103
|
+
!ts.isPropertyAccessExpression(statement.expression) ||
|
|
1104
|
+
!ts.isIdentifier(statement.expression.expression) ||
|
|
1105
|
+
statement.expression.expression.text !== optionsParameterName ||
|
|
1106
|
+
statement.expression.name.text !== 'check' ||
|
|
1107
|
+
statement.elseStatement ||
|
|
1108
|
+
!ts.isBlock(statement.thenStatement) ||
|
|
1109
|
+
statement.thenStatement.statements.length !== 1 ||
|
|
1110
|
+
!isDirectMethodCall(statement.thenStatement.statements[0], argsBinding, 'push', '--check')) {
|
|
1111
|
+
return [];
|
|
1112
|
+
}
|
|
1113
|
+
return [index];
|
|
1114
|
+
});
|
|
1115
|
+
if (checkGuardIndexes.length !== 1) {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
const [checkGuardIndex] = checkGuardIndexes;
|
|
1119
|
+
const spawnDeclarations = statements.flatMap((statement, index) => {
|
|
1120
|
+
if (!ts.isVariableStatement(statement) ||
|
|
1121
|
+
!(statement.declarationList.flags & ts.NodeFlags.Const) ||
|
|
1122
|
+
statement.declarationList.declarations.length !== 1) {
|
|
1123
|
+
return [];
|
|
1124
|
+
}
|
|
1125
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
1126
|
+
const call = declaration.initializer;
|
|
1127
|
+
return ts.isIdentifier(declaration.name) &&
|
|
1128
|
+
call !== undefined &&
|
|
1129
|
+
ts.isCallExpression(call) &&
|
|
1130
|
+
ts.isIdentifier(call.expression) &&
|
|
1131
|
+
spawnBindings.has(call.expression.text) &&
|
|
1132
|
+
call.arguments.length === 3 &&
|
|
1133
|
+
ts.isStringLiteralLike(call.arguments[0]) &&
|
|
1134
|
+
call.arguments[0].text === 'tsx' &&
|
|
1135
|
+
ts.isIdentifier(call.arguments[1]) &&
|
|
1136
|
+
call.arguments[1].text === argsBinding &&
|
|
1137
|
+
ts.isObjectLiteralExpression(call.arguments[2]) &&
|
|
1138
|
+
hasCanonicalSpawnOptions(call.arguments[2])
|
|
1139
|
+
? [{ binding: declaration.name.text, index }]
|
|
1140
|
+
: [];
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
if (spawnDeclarations.length !== 1) {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
const [{ binding: resultBinding, index: spawnIndex }] = spawnDeclarations;
|
|
1147
|
+
const errorGuardIndex = spawnIndex + 1;
|
|
1148
|
+
const statusGuardIndex = errorGuardIndex + 1;
|
|
1149
|
+
return (argsIndex === 0 &&
|
|
1150
|
+
checkGuardIndex === argsIndex + 1 &&
|
|
1151
|
+
spawnIndex === checkGuardIndex + 1 &&
|
|
1152
|
+
statusGuardIndex === statements.length - 1 &&
|
|
1153
|
+
!hasEarlierAbruptCompletion(statements, spawnIndex) &&
|
|
1154
|
+
hasCanonicalRunnerErrorGuard(sourceFile, statements[errorGuardIndex], resultBinding) &&
|
|
1155
|
+
hasCanonicalRunnerStatusGuard(statements[statusGuardIndex], resultBinding, scriptPathParameterName));
|
|
1156
|
+
}
|
|
1157
|
+
function hasTopLevelMainInvocation(sourceFile, allowedRuntimeImportModules) {
|
|
1158
|
+
const main = getSingleTopLevelFunction(sourceFile, 'main');
|
|
1159
|
+
if (!main ||
|
|
1160
|
+
main.parameters.length !== 0 ||
|
|
1161
|
+
main.asteriskToken !== undefined ||
|
|
1162
|
+
!main.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ||
|
|
1163
|
+
hasOverriddenTrustedSyncHelperGlobal(sourceFile)) {
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
const invocationIndexes = sourceFile.statements.flatMap((statement, statementIndex) => {
|
|
1167
|
+
if (!ts.isExpressionStatement(statement)) {
|
|
1168
|
+
return [];
|
|
1169
|
+
}
|
|
1170
|
+
const outerCall = statement.expression;
|
|
1171
|
+
if (!ts.isCallExpression(outerCall) ||
|
|
1172
|
+
!ts.isPropertyAccessExpression(outerCall.expression) ||
|
|
1173
|
+
outerCall.expression.name.text !== 'catch') {
|
|
1174
|
+
return [];
|
|
1175
|
+
}
|
|
1176
|
+
const mainCall = outerCall.expression.expression;
|
|
1177
|
+
if (!ts.isCallExpression(mainCall) ||
|
|
1178
|
+
!ts.isIdentifier(mainCall.expression) ||
|
|
1179
|
+
mainCall.expression.text !== 'main' ||
|
|
1180
|
+
mainCall.arguments.length !== 0 ||
|
|
1181
|
+
outerCall.arguments.length !== 1) {
|
|
1182
|
+
return [];
|
|
1183
|
+
}
|
|
1184
|
+
return isCanonicalSyncMainCatchHandler(outerCall.arguments[0])
|
|
1185
|
+
? [statementIndex]
|
|
1186
|
+
: [];
|
|
1187
|
+
});
|
|
1188
|
+
if (invocationIndexes.length !== 1) {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
const [invocationIndex] = invocationIndexes;
|
|
1192
|
+
const hasEarlierMainCall = sourceFile.statements
|
|
1193
|
+
.slice(0, invocationIndex)
|
|
1194
|
+
.some((statement) => containsCompletion(statement, (candidate) => {
|
|
1195
|
+
if (!ts.isCallExpression(candidate)) {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
const target = unwrapStaticExpression(candidate.expression);
|
|
1199
|
+
return ts.isIdentifier(target) && target.text === 'main';
|
|
1200
|
+
}));
|
|
1201
|
+
return (invocationIndex === sourceFile.statements.length - 1 &&
|
|
1202
|
+
sourceFile.statements
|
|
1203
|
+
.slice(0, invocationIndex)
|
|
1204
|
+
.every((statement) => isAllowedSyncHelperTopLevelStatement(statement, allowedRuntimeImportModules)) &&
|
|
1205
|
+
!hasEarlierAbruptCompletion(sourceFile.statements, invocationIndex) &&
|
|
1206
|
+
!hasEarlierMainCall);
|
|
1207
|
+
}
|
|
1208
|
+
function getDirectVariableBinding(statements, predicate) {
|
|
1209
|
+
const bindings = [];
|
|
1210
|
+
statements.forEach((statement, index) => {
|
|
1211
|
+
if (!ts.isVariableStatement(statement) ||
|
|
1212
|
+
!(statement.declarationList.flags & ts.NodeFlags.Const) ||
|
|
1213
|
+
statement.declarationList.declarations.length !== 1) {
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const declaration = statement.declarationList.declarations[0];
|
|
1217
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
1218
|
+
declaration.initializer &&
|
|
1219
|
+
predicate(declaration.initializer)) {
|
|
1220
|
+
bindings.push({ binding: declaration.name.text, index });
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
return bindings.length === 1 ? bindings[0] : null;
|
|
1224
|
+
}
|
|
1225
|
+
function isParseCliOptionsCall(expression) {
|
|
1226
|
+
if (!ts.isCallExpression(expression) ||
|
|
1227
|
+
!ts.isIdentifier(expression.expression) ||
|
|
1228
|
+
expression.expression.text !== 'parseCliOptions' ||
|
|
1229
|
+
expression.arguments.length !== 1) {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
const argvSlice = expression.arguments[0];
|
|
1233
|
+
return (ts.isCallExpression(argvSlice) &&
|
|
1234
|
+
ts.isPropertyAccessExpression(argvSlice.expression) &&
|
|
1235
|
+
argvSlice.expression.name.text === 'slice' &&
|
|
1236
|
+
ts.isPropertyAccessExpression(argvSlice.expression.expression) &&
|
|
1237
|
+
ts.isIdentifier(argvSlice.expression.expression.expression) &&
|
|
1238
|
+
argvSlice.expression.expression.expression.text === 'process' &&
|
|
1239
|
+
argvSlice.expression.expression.name.text === 'argv' &&
|
|
1240
|
+
argvSlice.arguments.length === 1 &&
|
|
1241
|
+
ts.isNumericLiteral(argvSlice.arguments[0]) &&
|
|
1242
|
+
argvSlice.arguments[0].text === '2');
|
|
1243
|
+
}
|
|
1244
|
+
function isCanonicalBooleanOptionAssignment(statement, optionsBinding, propertyName) {
|
|
1245
|
+
if (!ts.isExpressionStatement(statement) ||
|
|
1246
|
+
!ts.isBinaryExpression(statement.expression) ||
|
|
1247
|
+
statement.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
const { left, right } = statement.expression;
|
|
1251
|
+
return (isResultPropertyAccess(left, optionsBinding, propertyName) &&
|
|
1252
|
+
right.kind === ts.SyntaxKind.TrueKeyword);
|
|
1253
|
+
}
|
|
1254
|
+
function getCanonicalBooleanFlagGuardIndex(body, argumentBinding, optionsBinding, flagName, propertyName) {
|
|
1255
|
+
const guardIndexes = body.statements.flatMap((statement, index) => {
|
|
1256
|
+
if (!ts.isIfStatement(statement) ||
|
|
1257
|
+
statement.elseStatement ||
|
|
1258
|
+
!ts.isBinaryExpression(statement.expression) ||
|
|
1259
|
+
statement.expression.operatorToken.kind !==
|
|
1260
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
|
1261
|
+
!ts.isIdentifier(statement.expression.left) ||
|
|
1262
|
+
statement.expression.left.text !== argumentBinding ||
|
|
1263
|
+
!ts.isStringLiteralLike(statement.expression.right) ||
|
|
1264
|
+
statement.expression.right.text !== flagName ||
|
|
1265
|
+
!ts.isBlock(statement.thenStatement) ||
|
|
1266
|
+
statement.thenStatement.statements.length !== 2) {
|
|
1267
|
+
return [];
|
|
1268
|
+
}
|
|
1269
|
+
return (isCanonicalBooleanOptionAssignment(statement.thenStatement.statements[0], optionsBinding, propertyName) && ts.isContinueStatement(statement.thenStatement.statements[1]))
|
|
1270
|
+
? [index]
|
|
1271
|
+
: [];
|
|
1272
|
+
});
|
|
1273
|
+
return guardIndexes.length === 1 ? guardIndexes[0] : null;
|
|
1274
|
+
}
|
|
1275
|
+
function hasCanonicalCheckGuard(body, argumentBinding, optionsBinding) {
|
|
1276
|
+
const guardIndex = getCanonicalBooleanFlagGuardIndex(body, argumentBinding, optionsBinding, '--check', 'check');
|
|
1277
|
+
if (guardIndex === null) {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
return (!containsParserControlFlow(body, argumentBinding, 'outer-break-or-return') &&
|
|
1281
|
+
!hasEarlierAbruptCompletion(body.statements, guardIndex) &&
|
|
1282
|
+
!body.statements
|
|
1283
|
+
.slice(0, guardIndex)
|
|
1284
|
+
.some((statement) => containsParserControlFlow(statement, argumentBinding, 'unsafe-continue')));
|
|
1285
|
+
}
|
|
1286
|
+
function hasCanonicalAdditionalBooleanFlags(body, argumentBinding, optionsBinding, optionProperties, flags) {
|
|
1287
|
+
const checkGuardIndex = getCanonicalBooleanFlagGuardIndex(body, argumentBinding, optionsBinding, '--check', 'check');
|
|
1288
|
+
let previousGuardIndex = -1;
|
|
1289
|
+
return flags.every(({ flagName, propertyName }) => {
|
|
1290
|
+
if (optionProperties.get(propertyName)?.kind !==
|
|
1291
|
+
ts.SyntaxKind.FalseKeyword) {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
const guardIndex = getCanonicalBooleanFlagGuardIndex(body, argumentBinding, optionsBinding, flagName, propertyName);
|
|
1295
|
+
let assignmentCount = 0;
|
|
1296
|
+
function countAssignments(node) {
|
|
1297
|
+
if (ts.isFunctionLike(node)) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (ts.isStatement(node) &&
|
|
1301
|
+
isCanonicalBooleanOptionAssignment(node, optionsBinding, propertyName)) {
|
|
1302
|
+
assignmentCount += 1;
|
|
1303
|
+
}
|
|
1304
|
+
ts.forEachChild(node, countAssignments);
|
|
1305
|
+
}
|
|
1306
|
+
countAssignments(body);
|
|
1307
|
+
if (guardIndex === null ||
|
|
1308
|
+
checkGuardIndex === null ||
|
|
1309
|
+
guardIndex <= previousGuardIndex ||
|
|
1310
|
+
guardIndex >= checkGuardIndex ||
|
|
1311
|
+
assignmentCount !== 1 ||
|
|
1312
|
+
hasEarlierAbruptCompletion(body.statements, guardIndex) ||
|
|
1313
|
+
body.statements
|
|
1314
|
+
.slice(0, guardIndex)
|
|
1315
|
+
.some((statement) => containsParserControlFlow(statement, argumentBinding, 'unsafe-continue', 0, 0, false, new Map(), flagName))) {
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
previousGuardIndex = guardIndex;
|
|
1319
|
+
return true;
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
function getCanonicalForOfArgument(statement, argvBinding) {
|
|
1323
|
+
if (!ts.isIdentifier(statement.expression) ||
|
|
1324
|
+
statement.expression.text !== argvBinding ||
|
|
1325
|
+
!ts.isVariableDeclarationList(statement.initializer) ||
|
|
1326
|
+
!(statement.initializer.flags & ts.NodeFlags.Const) ||
|
|
1327
|
+
statement.initializer.declarations.length !== 1 ||
|
|
1328
|
+
!ts.isIdentifier(statement.initializer.declarations[0].name) ||
|
|
1329
|
+
!ts.isBlock(statement.statement)) {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
argumentBinding: statement.initializer.declarations[0].name.text,
|
|
1334
|
+
body: statement.statement,
|
|
1335
|
+
indexBinding: null,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
function getCanonicalIndexedArgument(statement, argvBinding) {
|
|
1339
|
+
const initializer = statement.initializer;
|
|
1340
|
+
if (!initializer ||
|
|
1341
|
+
!ts.isVariableDeclarationList(initializer) ||
|
|
1342
|
+
initializer.declarations.length !== 1 ||
|
|
1343
|
+
!ts.isIdentifier(initializer.declarations[0].name) ||
|
|
1344
|
+
!initializer.declarations[0].initializer ||
|
|
1345
|
+
!ts.isNumericLiteral(initializer.declarations[0].initializer) ||
|
|
1346
|
+
initializer.declarations[0].initializer.text !== '0' ||
|
|
1347
|
+
!statement.condition ||
|
|
1348
|
+
!ts.isBinaryExpression(statement.condition) ||
|
|
1349
|
+
statement.condition.operatorToken.kind !== ts.SyntaxKind.LessThanToken ||
|
|
1350
|
+
!ts.isIdentifier(statement.condition.left) ||
|
|
1351
|
+
!ts.isPropertyAccessExpression(statement.condition.right) ||
|
|
1352
|
+
!ts.isIdentifier(statement.condition.right.expression) ||
|
|
1353
|
+
statement.condition.right.expression.text !== argvBinding ||
|
|
1354
|
+
statement.condition.right.name.text !== 'length' ||
|
|
1355
|
+
!statement.incrementor ||
|
|
1356
|
+
!ts.isBinaryExpression(statement.incrementor) ||
|
|
1357
|
+
statement.incrementor.operatorToken.kind !== ts.SyntaxKind.PlusEqualsToken ||
|
|
1358
|
+
!ts.isIdentifier(statement.incrementor.left) ||
|
|
1359
|
+
!ts.isNumericLiteral(statement.incrementor.right) ||
|
|
1360
|
+
statement.incrementor.right.text !== '1' ||
|
|
1361
|
+
!ts.isBlock(statement.statement)) {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
const indexBinding = initializer.declarations[0].name.text;
|
|
1365
|
+
if (statement.condition.left.text !== indexBinding ||
|
|
1366
|
+
statement.incrementor.left.text !== indexBinding) {
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
const argumentDeclaration = getDirectVariableBinding(statement.statement.statements, (value) => ts.isElementAccessExpression(value) &&
|
|
1370
|
+
ts.isIdentifier(value.expression) &&
|
|
1371
|
+
value.expression.text === argvBinding &&
|
|
1372
|
+
value.argumentExpression !== undefined &&
|
|
1373
|
+
ts.isIdentifier(value.argumentExpression) &&
|
|
1374
|
+
value.argumentExpression.text === indexBinding);
|
|
1375
|
+
return argumentDeclaration
|
|
1376
|
+
? {
|
|
1377
|
+
argumentBinding: argumentDeclaration.binding,
|
|
1378
|
+
body: statement.statement,
|
|
1379
|
+
indexBinding,
|
|
1380
|
+
}
|
|
1381
|
+
: null;
|
|
1382
|
+
}
|
|
1383
|
+
function hasCanonicalReportFlagGuard(body, argvBinding, argumentBinding, optionsBinding, indexBinding) {
|
|
1384
|
+
if (indexBinding === null) {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
const expectedGuard = ts.createSourceFile('canonical-sync-report-parser.ts', `
|
|
1388
|
+
if (${argumentBinding} === '--report') {
|
|
1389
|
+
const reportMode = ${argvBinding}[${indexBinding} + 1];
|
|
1390
|
+
if (reportMode !== 'json') {
|
|
1391
|
+
throw new Error('The \`--report\` flag currently supports only \`json\`.');
|
|
1392
|
+
}
|
|
1393
|
+
${optionsBinding}.report = reportMode;
|
|
1394
|
+
${indexBinding} += 1;
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS).statements[0];
|
|
1398
|
+
if (!expectedGuard) {
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
const expectedFingerprint = getTypeScriptNodeFingerprint(expectedGuard);
|
|
1402
|
+
const guardIndexes = body.statements.flatMap((statement, index) => getTypeScriptNodeFingerprint(statement) === expectedFingerprint
|
|
1403
|
+
? [index]
|
|
1404
|
+
: []);
|
|
1405
|
+
if (guardIndexes.length !== 1) {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
let reportAssignmentCount = 0;
|
|
1409
|
+
function countReportAssignments(node) {
|
|
1410
|
+
if (ts.isFunctionLike(node)) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (ts.isBinaryExpression(node) &&
|
|
1414
|
+
isResultPropertyAccess(node.left, optionsBinding, 'report')) {
|
|
1415
|
+
reportAssignmentCount += 1;
|
|
1416
|
+
}
|
|
1417
|
+
ts.forEachChild(node, countReportAssignments);
|
|
1418
|
+
}
|
|
1419
|
+
countReportAssignments(body);
|
|
1420
|
+
const [guardIndex] = guardIndexes;
|
|
1421
|
+
return (reportAssignmentCount === 1 &&
|
|
1422
|
+
!hasEarlierAbruptCompletion(body.statements, guardIndex) &&
|
|
1423
|
+
!body.statements
|
|
1424
|
+
.slice(0, guardIndex)
|
|
1425
|
+
.some((statement) => containsParserControlFlow(statement, argumentBinding, 'unsafe-continue', 0, 0, false, new Map(), '--report')));
|
|
1426
|
+
}
|
|
1427
|
+
function countIdentifierOccurrences(node, binding) {
|
|
1428
|
+
let count = 0;
|
|
1429
|
+
function visit(candidate) {
|
|
1430
|
+
if (ts.isIdentifier(candidate) && candidate.text === binding) {
|
|
1431
|
+
count += 1;
|
|
1432
|
+
}
|
|
1433
|
+
ts.forEachChild(candidate, visit);
|
|
1434
|
+
}
|
|
1435
|
+
visit(node);
|
|
1436
|
+
return count;
|
|
1437
|
+
}
|
|
1438
|
+
function hasCanonicalCheckParser(sourceFile, additionalBooleanFlags = [], requiresReportMode = false) {
|
|
1439
|
+
const parser = getSingleTopLevelFunction(sourceFile, 'parseCliOptions');
|
|
1440
|
+
if (!parser?.body ||
|
|
1441
|
+
parser.parameters.length !== 1 ||
|
|
1442
|
+
!ts.isIdentifier(parser.parameters[0].name) ||
|
|
1443
|
+
parser.parameters[0].dotDotDotToken !== undefined ||
|
|
1444
|
+
parser.parameters[0].initializer !== undefined ||
|
|
1445
|
+
parser.body.statements.length !== 3 ||
|
|
1446
|
+
parser.asteriskToken !== undefined ||
|
|
1447
|
+
parser.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
const argvBinding = parser.parameters[0].name.text;
|
|
1451
|
+
const optionsStatement = parser.body.statements[0];
|
|
1452
|
+
if (!ts.isVariableStatement(optionsStatement) ||
|
|
1453
|
+
!(optionsStatement.declarationList.flags & ts.NodeFlags.Const) ||
|
|
1454
|
+
optionsStatement.declarationList.declarations.length !== 1) {
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
const optionsDeclaration = optionsStatement.declarationList.declarations[0];
|
|
1458
|
+
if (!ts.isIdentifier(optionsDeclaration.name) ||
|
|
1459
|
+
!optionsDeclaration.initializer ||
|
|
1460
|
+
!ts.isObjectLiteralExpression(optionsDeclaration.initializer)) {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
const optionProperties = new Map();
|
|
1464
|
+
for (const property of optionsDeclaration.initializer.properties) {
|
|
1465
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
1466
|
+
ts.isComputedPropertyName(property.name) ||
|
|
1467
|
+
(!ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name)) ||
|
|
1468
|
+
optionProperties.has(property.name.text)) {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
optionProperties.set(property.name.text, property.initializer);
|
|
1472
|
+
}
|
|
1473
|
+
const expectedPropertyCount = 1 + additionalBooleanFlags.length + (requiresReportMode ? 1 : 0);
|
|
1474
|
+
if (optionProperties.size !== expectedPropertyCount ||
|
|
1475
|
+
optionProperties.get('check')?.kind !== ts.SyntaxKind.FalseKeyword) {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
const loopStatement = parser.body.statements[1];
|
|
1479
|
+
const loop = ts.isForOfStatement(loopStatement)
|
|
1480
|
+
? getCanonicalForOfArgument(loopStatement, argvBinding)
|
|
1481
|
+
: ts.isForStatement(loopStatement)
|
|
1482
|
+
? getCanonicalIndexedArgument(loopStatement, argvBinding)
|
|
1483
|
+
: null;
|
|
1484
|
+
const returnStatement = parser.body.statements[2];
|
|
1485
|
+
const reportProperty = optionProperties.get('report');
|
|
1486
|
+
return (loop !== null &&
|
|
1487
|
+
hasOnlyCanonicalParserEffects(loop.body, optionsDeclaration.name.text, loop.indexBinding) &&
|
|
1488
|
+
hasCanonicalUnknownFlagThrow(loop.body, loop.argumentBinding) &&
|
|
1489
|
+
countOuterParserContinues(loop.body) === expectedPropertyCount &&
|
|
1490
|
+
countIdentifierOccurrences(loop.body, optionsDeclaration.name.text) === expectedPropertyCount &&
|
|
1491
|
+
(!requiresReportMode ||
|
|
1492
|
+
(reportProperty !== undefined &&
|
|
1493
|
+
ts.isStringLiteralLike(reportProperty) &&
|
|
1494
|
+
reportProperty.text === 'human' &&
|
|
1495
|
+
hasCanonicalReportFlagGuard(loop.body, argvBinding, loop.argumentBinding, optionsDeclaration.name.text, loop.indexBinding))) &&
|
|
1496
|
+
hasCanonicalCheckGuard(loop.body, loop.argumentBinding, optionsDeclaration.name.text) &&
|
|
1497
|
+
hasCanonicalAdditionalBooleanFlags(loop.body, loop.argumentBinding, optionsDeclaration.name.text, optionProperties, additionalBooleanFlags) &&
|
|
1498
|
+
ts.isReturnStatement(returnStatement) &&
|
|
1499
|
+
returnStatement.expression !== undefined &&
|
|
1500
|
+
ts.isIdentifier(returnStatement.expression) &&
|
|
1501
|
+
returnStatement.expression.text === optionsDeclaration.name.text);
|
|
1502
|
+
}
|
|
1503
|
+
function isCanonicalRestExistsCondition(expression, scriptPathBinding) {
|
|
1504
|
+
if (!ts.isCallExpression(expression) ||
|
|
1505
|
+
!ts.isPropertyAccessExpression(expression.expression) ||
|
|
1506
|
+
!ts.isIdentifier(expression.expression.expression) ||
|
|
1507
|
+
expression.expression.expression.text !== 'fs' ||
|
|
1508
|
+
expression.expression.name.text !== 'existsSync' ||
|
|
1509
|
+
expression.arguments.length !== 1) {
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
const resolvedPath = expression.arguments[0];
|
|
1513
|
+
return (ts.isCallExpression(resolvedPath) &&
|
|
1514
|
+
ts.isPropertyAccessExpression(resolvedPath.expression) &&
|
|
1515
|
+
ts.isIdentifier(resolvedPath.expression.expression) &&
|
|
1516
|
+
resolvedPath.expression.expression.text === 'path' &&
|
|
1517
|
+
resolvedPath.expression.name.text === 'resolve' &&
|
|
1518
|
+
resolvedPath.arguments.length === 2 &&
|
|
1519
|
+
ts.isCallExpression(resolvedPath.arguments[0]) &&
|
|
1520
|
+
ts.isPropertyAccessExpression(resolvedPath.arguments[0].expression) &&
|
|
1521
|
+
ts.isIdentifier(resolvedPath.arguments[0].expression.expression) &&
|
|
1522
|
+
resolvedPath.arguments[0].expression.expression.text === 'process' &&
|
|
1523
|
+
resolvedPath.arguments[0].expression.name.text === 'cwd' &&
|
|
1524
|
+
resolvedPath.arguments[0].arguments.length === 0 &&
|
|
1525
|
+
ts.isIdentifier(resolvedPath.arguments[1]) &&
|
|
1526
|
+
resolvedPath.arguments[1].text === scriptPathBinding);
|
|
1527
|
+
}
|
|
1528
|
+
function getCanonicalSyncProjectDelegationIndex(sourceFile, expectedScriptPath, requiresExistsGuard) {
|
|
1529
|
+
const main = getSingleTopLevelFunction(sourceFile, 'main');
|
|
1530
|
+
if (!main?.body ||
|
|
1531
|
+
!hasTopLevelMainInvocation(sourceFile, new Set(['node:child_process', 'node:fs', 'node:path'])) ||
|
|
1532
|
+
hasImportedBinding(sourceFile, 'console') ||
|
|
1533
|
+
hasShadowedBinding(sourceFile, new Set(['console'])) ||
|
|
1534
|
+
hasShadowedBinding(main, new Set(['fs', 'parseCliOptions', 'path', 'runSyncScript'])) ||
|
|
1535
|
+
!hasCanonicalDefaultImport(sourceFile, 'node:fs', 'fs') ||
|
|
1536
|
+
!hasCanonicalDefaultImport(sourceFile, 'node:path', 'path')) {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
const statements = [...main.body.statements];
|
|
1540
|
+
const optionsDeclaration = getDirectVariableBinding(statements, isParseCliOptionsCall);
|
|
1541
|
+
const scriptPathDeclaration = getDirectVariableBinding(statements, (initializer) => isSyncScriptPathExpression(initializer, expectedScriptPath));
|
|
1542
|
+
if (!optionsDeclaration || !scriptPathDeclaration) {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
const optionsBinding = optionsDeclaration.binding;
|
|
1546
|
+
const scriptPathBinding = scriptPathDeclaration.binding;
|
|
1547
|
+
if (!requiresExistsGuard) {
|
|
1548
|
+
const delegationIndexes = statements.flatMap((statement, index) => isDirectRunSyncCall(statement, scriptPathBinding, optionsBinding)
|
|
1549
|
+
? [index]
|
|
1550
|
+
: []);
|
|
1551
|
+
return delegationIndexes.length === 1 &&
|
|
1552
|
+
optionsDeclaration.index < delegationIndexes[0] &&
|
|
1553
|
+
scriptPathDeclaration.index < delegationIndexes[0] &&
|
|
1554
|
+
!hasEarlierAbruptCompletion(statements, delegationIndexes[0])
|
|
1555
|
+
? delegationIndexes[0]
|
|
1556
|
+
: null;
|
|
1557
|
+
}
|
|
1558
|
+
const guardedDelegationIndexes = statements.flatMap((statement, index) => {
|
|
1559
|
+
if (!ts.isIfStatement(statement) ||
|
|
1560
|
+
statement.elseStatement ||
|
|
1561
|
+
!isCanonicalRestExistsCondition(statement.expression, scriptPathBinding) ||
|
|
1562
|
+
!ts.isBlock(statement.thenStatement) ||
|
|
1563
|
+
statement.thenStatement.statements.length !== 1 ||
|
|
1564
|
+
!isDirectRunSyncCall(statement.thenStatement.statements[0], scriptPathBinding, optionsBinding)) {
|
|
1565
|
+
return [];
|
|
1566
|
+
}
|
|
1567
|
+
return [index];
|
|
1568
|
+
});
|
|
1569
|
+
return guardedDelegationIndexes.length === 1 &&
|
|
1570
|
+
optionsDeclaration.index < guardedDelegationIndexes[0] &&
|
|
1571
|
+
scriptPathDeclaration.index < guardedDelegationIndexes[0] &&
|
|
1572
|
+
!hasEarlierAbruptCompletion(statements, guardedDelegationIndexes[0])
|
|
1573
|
+
? guardedDelegationIndexes[0]
|
|
1574
|
+
: null;
|
|
1575
|
+
}
|
|
1576
|
+
function isCanonicalSyncProjectCompletionLog(statement, optionsBinding) {
|
|
1577
|
+
const call = statement ? getDirectCall(statement) : null;
|
|
1578
|
+
if (!call ||
|
|
1579
|
+
!ts.isPropertyAccessExpression(call.expression) ||
|
|
1580
|
+
!ts.isIdentifier(call.expression.expression) ||
|
|
1581
|
+
call.expression.expression.text !== 'console' ||
|
|
1582
|
+
call.expression.name.text !== 'log' ||
|
|
1583
|
+
call.arguments.length !== 1) {
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
const message = unwrapStaticExpression(call.arguments[0]);
|
|
1587
|
+
if (!ts.isConditionalExpression(message) ||
|
|
1588
|
+
!isResultPropertyAccess(message.condition, optionsBinding, 'check')) {
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
const whenTrue = unwrapStaticExpression(message.whenTrue);
|
|
1592
|
+
const whenFalse = unwrapStaticExpression(message.whenFalse);
|
|
1593
|
+
return ts.isStringLiteralLike(whenTrue) && ts.isStringLiteralLike(whenFalse);
|
|
1594
|
+
}
|
|
1595
|
+
function getSyncProjectDelegationProblem(project, requiresRest) {
|
|
1596
|
+
const syncProjectPath = path.join(project.projectDir, STANDALONE_SYNC_PROJECT_SCRIPT);
|
|
1597
|
+
if (!fs.existsSync(syncProjectPath)) {
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
let source;
|
|
1601
|
+
try {
|
|
1602
|
+
source = fs.readFileSync(syncProjectPath, 'utf8');
|
|
1603
|
+
}
|
|
1604
|
+
catch {
|
|
1605
|
+
return `Unable to read generated helper ${STANDALONE_SYNC_PROJECT_SCRIPT}`;
|
|
1606
|
+
}
|
|
1607
|
+
if (hasTypeScriptSyntaxErrors(source, syncProjectPath)) {
|
|
1608
|
+
return `${STANDALONE_SYNC_PROJECT_SCRIPT} contains TypeScript syntax errors.`;
|
|
1609
|
+
}
|
|
1610
|
+
const sourceFile = ts.createSourceFile(syncProjectPath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
1611
|
+
if (!hasCanonicalCheckParser(sourceFile)) {
|
|
1612
|
+
return `${STANDALONE_SYNC_PROJECT_SCRIPT} must parse and forward --check through the canonical tsx runner.`;
|
|
1613
|
+
}
|
|
1614
|
+
if (!hasCanonicalSyncRunner(sourceFile)) {
|
|
1615
|
+
return `${STANDALONE_SYNC_PROJECT_SCRIPT} must forward --check through the canonical tsx runner.`;
|
|
1616
|
+
}
|
|
1617
|
+
const typeDelegationIndex = getCanonicalSyncProjectDelegationIndex(sourceFile, STANDALONE_SYNC_SCRIPT, false);
|
|
1618
|
+
if (typeDelegationIndex === null) {
|
|
1619
|
+
return `${STANDALONE_SYNC_PROJECT_SCRIPT} must delegate to ${STANDALONE_SYNC_SCRIPT} through the canonical tsx runner.`;
|
|
1620
|
+
}
|
|
1621
|
+
const main = getSingleTopLevelFunction(sourceFile, 'main');
|
|
1622
|
+
const mainStatements = main?.body?.statements;
|
|
1623
|
+
const optionsDeclaration = mainStatements
|
|
1624
|
+
? getDirectVariableBinding(mainStatements, isParseCliOptionsCall)
|
|
1625
|
+
: null;
|
|
1626
|
+
const typeScriptPathDeclaration = mainStatements
|
|
1627
|
+
? getDirectVariableBinding(mainStatements, (initializer) => isSyncScriptPathExpression(initializer, STANDALONE_SYNC_SCRIPT))
|
|
1628
|
+
: null;
|
|
1629
|
+
const restScriptPathDeclaration = mainStatements
|
|
1630
|
+
? getDirectVariableBinding(mainStatements, (initializer) => isSyncScriptPathExpression(initializer, STANDALONE_SYNC_REST_SCRIPT))
|
|
1631
|
+
: null;
|
|
1632
|
+
if (!mainStatements || !optionsDeclaration || !typeScriptPathDeclaration) {
|
|
1633
|
+
return `${STANDALONE_SYNC_PROJECT_SCRIPT} must keep its canonical sync completion flow.`;
|
|
1634
|
+
}
|
|
1635
|
+
const restDelegationIndex = getCanonicalSyncProjectDelegationIndex(sourceFile, STANDALONE_SYNC_REST_SCRIPT, true);
|
|
1636
|
+
if (requiresRest &&
|
|
1637
|
+
(restDelegationIndex === null ||
|
|
1638
|
+
restDelegationIndex <= typeDelegationIndex)) {
|
|
1639
|
+
return `${STANDALONE_SYNC_PROJECT_SCRIPT} must delegate to ${STANDALONE_SYNC_REST_SCRIPT} through the canonical tsx runner after the type sync.`;
|
|
1640
|
+
}
|
|
1641
|
+
const completionIndex = mainStatements.length - 1;
|
|
1642
|
+
const hasTypeOnlyTail = !requiresRest &&
|
|
1643
|
+
restDelegationIndex === null &&
|
|
1644
|
+
restScriptPathDeclaration === null &&
|
|
1645
|
+
optionsDeclaration.index === 0 &&
|
|
1646
|
+
typeScriptPathDeclaration.index === 1 &&
|
|
1647
|
+
typeDelegationIndex === 2 &&
|
|
1648
|
+
completionIndex === 3 &&
|
|
1649
|
+
isCanonicalSyncProjectCompletionLog(mainStatements[completionIndex], optionsDeclaration.binding);
|
|
1650
|
+
// Basic scaffolds intentionally retain the guarded REST delegation so they
|
|
1651
|
+
// can gain persistence later without replacing the project sync runner.
|
|
1652
|
+
const hasRestTail = restScriptPathDeclaration !== null &&
|
|
1653
|
+
optionsDeclaration.index === 0 &&
|
|
1654
|
+
typeScriptPathDeclaration.index === 1 &&
|
|
1655
|
+
restScriptPathDeclaration.index === 2 &&
|
|
1656
|
+
typeDelegationIndex === 3 &&
|
|
1657
|
+
restDelegationIndex === 4 &&
|
|
1658
|
+
completionIndex === 5 &&
|
|
1659
|
+
isCanonicalSyncProjectCompletionLog(mainStatements[completionIndex], optionsDeclaration.binding);
|
|
1660
|
+
return hasTypeOnlyTail || hasRestTail
|
|
1661
|
+
? null
|
|
1662
|
+
: `${STANDALONE_SYNC_PROJECT_SCRIPT} must keep only the canonical optional REST delegation and completion report after type sync.`;
|
|
1663
|
+
}
|
|
1664
|
+
const SHELL_DIRECTORY_CHANGE_COMMANDS = new Set([
|
|
1665
|
+
'.',
|
|
1666
|
+
'cd',
|
|
1667
|
+
'chdir',
|
|
1668
|
+
'popd',
|
|
1669
|
+
'pushd',
|
|
1670
|
+
'source',
|
|
1671
|
+
]);
|
|
1672
|
+
const SHELL_COMMAND_PREFIXES = new Set([
|
|
1673
|
+
'!',
|
|
1674
|
+
'{',
|
|
1675
|
+
'do',
|
|
1676
|
+
'elif',
|
|
1677
|
+
'else',
|
|
1678
|
+
'if',
|
|
1679
|
+
'then',
|
|
1680
|
+
'time',
|
|
1681
|
+
'until',
|
|
1682
|
+
'while',
|
|
1683
|
+
]);
|
|
1684
|
+
function splitShellCommandSegments(script) {
|
|
1685
|
+
const segments = [];
|
|
1686
|
+
let current = '';
|
|
1687
|
+
let quote = null;
|
|
1688
|
+
let startsShellWord = true;
|
|
1689
|
+
function pushCurrent(operatorAfter) {
|
|
1690
|
+
const normalized = current.replace(/\s+/gu, ' ').trim();
|
|
1691
|
+
if (normalized.length > 0) {
|
|
1692
|
+
segments.push({ command: normalized, operatorAfter });
|
|
1693
|
+
}
|
|
1694
|
+
current = '';
|
|
1695
|
+
startsShellWord = true;
|
|
1696
|
+
}
|
|
1697
|
+
for (let index = 0; index < script.length; index += 1) {
|
|
1698
|
+
const character = script[index];
|
|
1699
|
+
if (character === '\\' && quote !== "'") {
|
|
1700
|
+
current += character;
|
|
1701
|
+
if (script[index + 1] !== undefined) {
|
|
1702
|
+
current += script[index + 1];
|
|
1703
|
+
index += 1;
|
|
1704
|
+
}
|
|
1705
|
+
startsShellWord = false;
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
if (quote) {
|
|
1709
|
+
current += character;
|
|
1710
|
+
if (character === quote) {
|
|
1711
|
+
quote = null;
|
|
1712
|
+
}
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (character === "'" || character === '"') {
|
|
1716
|
+
quote = character;
|
|
1717
|
+
current += character;
|
|
1718
|
+
startsShellWord = false;
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (character === '#' && startsShellWord) {
|
|
1722
|
+
while (index + 1 < script.length &&
|
|
1723
|
+
script[index + 1] !== '\n' &&
|
|
1724
|
+
script[index + 1] !== '\r') {
|
|
1725
|
+
index += 1;
|
|
1726
|
+
}
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
const isTwoCharacterOperator = (character === '&' && script[index + 1] === '&') ||
|
|
1730
|
+
(character === '|' &&
|
|
1731
|
+
(script[index + 1] === '|' || script[index + 1] === '&'));
|
|
1732
|
+
const isBackgroundOperator = character === '&' &&
|
|
1733
|
+
script[index - 1] !== '>' &&
|
|
1734
|
+
script[index - 1] !== '<' &&
|
|
1735
|
+
script[index + 1] !== '>';
|
|
1736
|
+
const isPipelineOperator = character === '|' && script[index - 1] !== '>';
|
|
1737
|
+
if (character === ';' ||
|
|
1738
|
+
character === '\n' ||
|
|
1739
|
+
character === '\r' ||
|
|
1740
|
+
isTwoCharacterOperator ||
|
|
1741
|
+
isBackgroundOperator ||
|
|
1742
|
+
isPipelineOperator) {
|
|
1743
|
+
let operatorAfter;
|
|
1744
|
+
if (character === '\n' || character === '\r') {
|
|
1745
|
+
operatorAfter = ';';
|
|
1746
|
+
}
|
|
1747
|
+
else if (isTwoCharacterOperator) {
|
|
1748
|
+
operatorAfter = `${character}${script[index + 1]}`;
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
operatorAfter = character;
|
|
1752
|
+
}
|
|
1753
|
+
pushCurrent(operatorAfter);
|
|
1754
|
+
if (isTwoCharacterOperator) {
|
|
1755
|
+
index += 1;
|
|
1756
|
+
}
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
current += character;
|
|
1760
|
+
startsShellWord = character === ' ' || character === '\t';
|
|
1761
|
+
}
|
|
1762
|
+
pushCurrent(null);
|
|
1763
|
+
return segments;
|
|
1764
|
+
}
|
|
1765
|
+
function shellCommandMatches(segment, command, allowTrailingArguments = true) {
|
|
1766
|
+
return (segment.command === command ||
|
|
1767
|
+
(allowTrailingArguments && segment.command.startsWith(`${command} `)));
|
|
1768
|
+
}
|
|
1769
|
+
function getShellCommandWords(command) {
|
|
1770
|
+
const words = [];
|
|
1771
|
+
let current = '';
|
|
1772
|
+
let hasWord = false;
|
|
1773
|
+
let quote = null;
|
|
1774
|
+
function pushCurrent() {
|
|
1775
|
+
if (hasWord) {
|
|
1776
|
+
words.push(current);
|
|
1777
|
+
}
|
|
1778
|
+
current = '';
|
|
1779
|
+
hasWord = false;
|
|
1780
|
+
}
|
|
1781
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
1782
|
+
const character = command[index];
|
|
1783
|
+
if (quote) {
|
|
1784
|
+
if (character === quote) {
|
|
1785
|
+
quote = null;
|
|
1786
|
+
hasWord = true;
|
|
1787
|
+
}
|
|
1788
|
+
else if (character === '\\' &&
|
|
1789
|
+
quote === '"' &&
|
|
1790
|
+
command[index + 1] !== undefined) {
|
|
1791
|
+
current += command[index + 1];
|
|
1792
|
+
hasWord = true;
|
|
1793
|
+
index += 1;
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
current += character;
|
|
1797
|
+
hasWord = true;
|
|
1798
|
+
}
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
if (character === '\\' && command[index + 1] !== undefined) {
|
|
1802
|
+
current += command[index + 1];
|
|
1803
|
+
hasWord = true;
|
|
1804
|
+
index += 1;
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
if (character === "'" || character === '"') {
|
|
1808
|
+
quote = character;
|
|
1809
|
+
hasWord = true;
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
if (character === ' ' || character === '\t') {
|
|
1813
|
+
pushCurrent();
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
if (character === '{' || character === '}') {
|
|
1817
|
+
pushCurrent();
|
|
1818
|
+
words.push(character);
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
current += character;
|
|
1822
|
+
hasWord = true;
|
|
1823
|
+
}
|
|
1824
|
+
pushCurrent();
|
|
1825
|
+
return words;
|
|
1826
|
+
}
|
|
1827
|
+
function shellCommandChangesDirectory(segment) {
|
|
1828
|
+
const words = getShellCommandWords(segment.command);
|
|
1829
|
+
let commandIndex = 0;
|
|
1830
|
+
while (SHELL_COMMAND_PREFIXES.has(words[commandIndex] ?? '') ||
|
|
1831
|
+
/^[A-Za-z_][A-Za-z0-9_]*=/u.test(words[commandIndex] ?? '')) {
|
|
1832
|
+
commandIndex += 1;
|
|
1833
|
+
}
|
|
1834
|
+
if (words[commandIndex] === 'builtin' ||
|
|
1835
|
+
words[commandIndex] === 'command') {
|
|
1836
|
+
commandIndex += 1;
|
|
1837
|
+
while (words[commandIndex]?.startsWith('-') &&
|
|
1838
|
+
words[commandIndex] !== '-') {
|
|
1839
|
+
if (words[commandIndex] === '--') {
|
|
1840
|
+
commandIndex += 1;
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
commandIndex += 1;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const commandWord = words[commandIndex] ?? '';
|
|
1847
|
+
if (commandWord === 'function' ||
|
|
1848
|
+
/^[A-Za-z_][A-Za-z0-9_]*\(\)$/u.test(commandWord) ||
|
|
1849
|
+
(/^[A-Za-z_][A-Za-z0-9_]*$/u.test(commandWord) &&
|
|
1850
|
+
words[commandIndex + 1] === '()' &&
|
|
1851
|
+
words[commandIndex + 2] === '{')) {
|
|
1852
|
+
return true;
|
|
1853
|
+
}
|
|
1854
|
+
if (/[$`]/u.test(commandWord)) {
|
|
1855
|
+
return true;
|
|
1856
|
+
}
|
|
1857
|
+
if (commandWord === 'eval') {
|
|
1858
|
+
const evaluatedWords = getShellCommandWords(words.slice(commandIndex + 1).join(' '));
|
|
1859
|
+
// Static environment assignments cannot change cwd. Any executable or
|
|
1860
|
+
// dynamic eval body is intentionally treated as an unknown shell context.
|
|
1861
|
+
return (evaluatedWords.length > 0 &&
|
|
1862
|
+
!evaluatedWords.every((word) => /^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(word)));
|
|
1863
|
+
}
|
|
1864
|
+
return SHELL_DIRECTORY_CHANGE_COMMANDS.has(commandWord);
|
|
1865
|
+
}
|
|
1866
|
+
function getShellSegmentStaticReachability(segments) {
|
|
1867
|
+
const reachability = [];
|
|
1868
|
+
let terminated = false;
|
|
1869
|
+
let blockedByFalse = false;
|
|
1870
|
+
for (const [index, segment] of segments.entries()) {
|
|
1871
|
+
if (index === 0 || segments[index - 1].operatorAfter !== '&&') {
|
|
1872
|
+
blockedByFalse = false;
|
|
1873
|
+
}
|
|
1874
|
+
else {
|
|
1875
|
+
blockedByFalse =
|
|
1876
|
+
blockedByFalse || segments[index - 1].command === 'false';
|
|
1877
|
+
}
|
|
1878
|
+
const reachable = !terminated && !blockedByFalse;
|
|
1879
|
+
reachability.push(reachable);
|
|
1880
|
+
if (reachable &&
|
|
1881
|
+
(segment.command === 'exit' || segment.command.startsWith('exit ')) &&
|
|
1882
|
+
segment.operatorAfter !== '&' &&
|
|
1883
|
+
segment.operatorAfter !== '|' &&
|
|
1884
|
+
segment.operatorAfter !== '|&') {
|
|
1885
|
+
terminated = true;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return reachability;
|
|
1889
|
+
}
|
|
1890
|
+
function shellScriptInvokesCommand(script, command, allowTrailingArguments = true) {
|
|
1891
|
+
const segments = splitShellCommandSegments(script);
|
|
1892
|
+
const reachability = getShellSegmentStaticReachability(segments);
|
|
1893
|
+
let hasReachableDirectoryChange = false;
|
|
1894
|
+
for (const [index, segment] of segments.entries()) {
|
|
1895
|
+
if (shellCommandMatches(segment, command, allowTrailingArguments) &&
|
|
1896
|
+
reachability[index] === true &&
|
|
1897
|
+
!hasReachableDirectoryChange) {
|
|
1898
|
+
return true;
|
|
1899
|
+
}
|
|
1900
|
+
if (reachability[index] === true &&
|
|
1901
|
+
shellCommandChangesDirectory(segment)) {
|
|
1902
|
+
hasReachableDirectoryChange = true;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
function shellScriptPropagatesCommandFailure(script, command, allowTrailingArguments = true) {
|
|
1908
|
+
const segments = splitShellCommandSegments(script);
|
|
1909
|
+
const reachability = getShellSegmentStaticReachability(segments);
|
|
1910
|
+
return segments.some((segment, index) => {
|
|
1911
|
+
if (!shellCommandMatches(segment, command, allowTrailingArguments)) {
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1914
|
+
if (!reachability[index]) {
|
|
1915
|
+
return false;
|
|
1916
|
+
}
|
|
1917
|
+
if (index > 0 && segments[index - 1].operatorAfter === '||') {
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
for (let chainIndex = index; chainIndex < segments.length; chainIndex += 1) {
|
|
1921
|
+
const operatorAfter = segments[chainIndex].operatorAfter;
|
|
1922
|
+
if (operatorAfter === null) {
|
|
1923
|
+
return true;
|
|
1924
|
+
}
|
|
1925
|
+
if (operatorAfter === '||') {
|
|
1926
|
+
const fallback = segments[chainIndex + 1];
|
|
1927
|
+
if (!fallback) {
|
|
1928
|
+
return false;
|
|
1929
|
+
}
|
|
1930
|
+
const exitMatch = /^exit(?:\s+([0-9]+))?$/u.exec(fallback.command);
|
|
1931
|
+
const exitCode = exitMatch?.[1]
|
|
1932
|
+
? Number.parseInt(exitMatch[1], 10)
|
|
1933
|
+
: null;
|
|
1934
|
+
return (exitMatch !== null &&
|
|
1935
|
+
fallback.operatorAfter !== '&' &&
|
|
1936
|
+
fallback.operatorAfter !== '|' &&
|
|
1937
|
+
fallback.operatorAfter !== '|&' &&
|
|
1938
|
+
(exitCode === null || (exitCode >= 1 && exitCode <= 255)));
|
|
1939
|
+
}
|
|
1940
|
+
if (operatorAfter !== '&&') {
|
|
1941
|
+
return false;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return false;
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
function shellScriptRunsCommandAfterPrerequisite(script, prerequisiteCommand, targetCommand) {
|
|
1948
|
+
const segments = splitShellCommandSegments(script);
|
|
1949
|
+
let prerequisiteInAndList = false;
|
|
1950
|
+
let foundTarget = false;
|
|
1951
|
+
for (const segment of segments) {
|
|
1952
|
+
if (shellCommandMatches(segment, prerequisiteCommand, false)) {
|
|
1953
|
+
prerequisiteInAndList = true;
|
|
1954
|
+
}
|
|
1955
|
+
if (shellCommandMatches(segment, targetCommand)) {
|
|
1956
|
+
if (!prerequisiteInAndList) {
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
foundTarget = true;
|
|
1960
|
+
}
|
|
1961
|
+
if (segment.operatorAfter !== '&&') {
|
|
1962
|
+
prerequisiteInAndList = false;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return foundTarget;
|
|
1966
|
+
}
|
|
1967
|
+
function getPackageManagerSelector(project) {
|
|
1968
|
+
return typeof project.packageJson.packageManager === 'string'
|
|
1969
|
+
? project.packageJson.packageManager
|
|
1970
|
+
: undefined;
|
|
1971
|
+
}
|
|
1972
|
+
function getPackageMetadataCheck(project, requiresRest, requiresInteractivity) {
|
|
1973
|
+
const issues = [];
|
|
1974
|
+
if (typeof project.packageJson.name !== 'string' ||
|
|
1975
|
+
project.packageJson.name.trim().length === 0) {
|
|
1976
|
+
issues.push('package.json must define a non-empty string name');
|
|
1977
|
+
}
|
|
1978
|
+
else if (!getSafePackageBaseName(project.packageJson.name.trim())) {
|
|
1979
|
+
issues.push('package.json name must use a safe npm package name');
|
|
1980
|
+
}
|
|
1981
|
+
if (project.packageJson.packageManager !== undefined &&
|
|
1982
|
+
typeof project.packageJson.packageManager !== 'string') {
|
|
1983
|
+
issues.push('package.json packageManager must be a string when defined');
|
|
1984
|
+
}
|
|
1985
|
+
const packageManager = inferPackageManagerId(project.projectDir, getPackageManagerSelector(project));
|
|
1986
|
+
const syncCheckCommand = formatRunScript(packageManager, 'sync', '--check');
|
|
1987
|
+
const syncCommand = formatRunScript(packageManager, 'sync');
|
|
1988
|
+
const scriptRequirements = [
|
|
1989
|
+
{
|
|
1990
|
+
allowTrailingArguments: false,
|
|
1991
|
+
commands: ['tsx scripts/sync-project.ts'],
|
|
1992
|
+
name: 'sync',
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
allowTrailingArguments: false,
|
|
1996
|
+
commands: ['tsx scripts/sync-types-to-block-json.ts'],
|
|
1997
|
+
name: 'sync-types',
|
|
1998
|
+
},
|
|
1999
|
+
...(requiresRest
|
|
2000
|
+
? [
|
|
2001
|
+
{
|
|
2002
|
+
allowTrailingArguments: false,
|
|
2003
|
+
commands: ['tsx scripts/sync-rest-contracts.ts'],
|
|
2004
|
+
name: 'sync-rest',
|
|
2005
|
+
},
|
|
2006
|
+
]
|
|
2007
|
+
: []),
|
|
2008
|
+
{
|
|
2009
|
+
allowTrailingArguments: true,
|
|
2010
|
+
commands: [syncCommand, 'wp-scripts start'],
|
|
2011
|
+
name: 'start',
|
|
2012
|
+
orderedPrerequisite: syncCommand,
|
|
2013
|
+
orderedTarget: 'wp-scripts start',
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
allowTrailingArguments: true,
|
|
2017
|
+
commands: [syncCheckCommand, 'wp-scripts build'],
|
|
2018
|
+
name: 'build',
|
|
2019
|
+
orderedPrerequisite: syncCheckCommand,
|
|
2020
|
+
orderedTarget: 'wp-scripts build',
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
allowTrailingArguments: true,
|
|
2024
|
+
commands: [syncCheckCommand, 'tsc --noEmit'],
|
|
2025
|
+
name: 'typecheck',
|
|
2026
|
+
orderedPrerequisite: syncCheckCommand,
|
|
2027
|
+
orderedTarget: 'tsc --noEmit',
|
|
2028
|
+
},
|
|
2029
|
+
];
|
|
2030
|
+
for (const requirement of scriptRequirements) {
|
|
2031
|
+
const script = project.packageJson.scripts?.[requirement.name];
|
|
2032
|
+
if (typeof script !== 'string') {
|
|
2033
|
+
const scriptName = requirement.name;
|
|
2034
|
+
issues.push(`package.json must define the ${scriptName} script`);
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
for (const command of requirement.commands) {
|
|
2038
|
+
const invokesCommand = shellScriptInvokesCommand(script, command, requirement.allowTrailingArguments);
|
|
2039
|
+
if (!invokesCommand) {
|
|
2040
|
+
issues.push(`package.json ${requirement.name} script must invoke \`${command}\``);
|
|
2041
|
+
}
|
|
2042
|
+
else if (!shellScriptPropagatesCommandFailure(script, command, requirement.allowTrailingArguments)) {
|
|
2043
|
+
issues.push(`package.json ${requirement.name} script must propagate failures from \`${command}\``);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
if ('orderedTarget' in requirement &&
|
|
2047
|
+
requirement.commands.every((command) => shellScriptInvokesCommand(script, command, requirement.allowTrailingArguments)) &&
|
|
2048
|
+
!shellScriptRunsCommandAfterPrerequisite(script, requirement.orderedPrerequisite, requirement.orderedTarget)) {
|
|
2049
|
+
issues.push(`package.json ${requirement.name} script must run \`${requirement.orderedPrerequisite}\` before \`${requirement.orderedTarget}\` in the same && command list`);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
for (const packageName of [
|
|
2053
|
+
...REQUIRED_RUNTIME_PACKAGES,
|
|
2054
|
+
...REQUIRED_WORDPRESS_RUNTIME_PACKAGES,
|
|
2055
|
+
...(requiresInteractivity
|
|
2056
|
+
? REQUIRED_INTERACTIVITY_RUNTIME_PACKAGES
|
|
2057
|
+
: []),
|
|
2058
|
+
...(requiresRest ? REQUIRED_REST_RUNTIME_PACKAGES : []),
|
|
2059
|
+
...(requiresRest ? REQUIRED_REST_WORDPRESS_RUNTIME_PACKAGES : []),
|
|
2060
|
+
'@typia/unplugin',
|
|
2061
|
+
'@wordpress/scripts',
|
|
2062
|
+
'tsx',
|
|
2063
|
+
'typescript',
|
|
2064
|
+
]) {
|
|
2065
|
+
if (typeof getDeclaredDependency(project.packageJson, packageName) !==
|
|
2066
|
+
'string') {
|
|
2067
|
+
issues.push(`package.json must declare ${packageName}`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return createDoctorCheck('Standalone package metadata', issues.length === 0 ? 'pass' : 'fail', issues.length === 0
|
|
2071
|
+
? `package.json exposes the supported single-block sync surface for ${project.packageName}`
|
|
2072
|
+
: issues.join('; '), STANDALONE_DOCTOR_CODES.PACKAGE);
|
|
2073
|
+
}
|
|
2074
|
+
const PHP_BUILD_DIRECTORY_EXPRESSION = String.raw `__DIR__\s*\.\s*(?:'\/build'|"\/build")`;
|
|
2075
|
+
function getPhpFileBraceDepth(source, offset) {
|
|
2076
|
+
return getPhpCodeBraceDepth(source, offset, { requirePhpOpenTag: true });
|
|
2077
|
+
}
|
|
2078
|
+
function getPhpRangeMatchDepth(source, range, match) {
|
|
2079
|
+
return getPhpFileBraceDepth(source, range.start + (match.index ?? 0));
|
|
2080
|
+
}
|
|
2081
|
+
function hasEarlierDirectPhpCompletion(source, start, end) {
|
|
2082
|
+
return [
|
|
2083
|
+
...source.slice(start, end).matchAll(/\b(?:return|throw|exit|die)\b/gu),
|
|
2084
|
+
].some((match) => getPhpFileBraceDepth(source, start + (match.index ?? 0)) === 1);
|
|
2085
|
+
}
|
|
2086
|
+
function hasPhpVariableReassignment(source, variableName, start, end) {
|
|
2087
|
+
const escapedVariableName = escapeRegExp(variableName);
|
|
2088
|
+
const assignmentPattern = new RegExp(String.raw `\$${escapedVariableName}\s*(?:\?\?=|<<=|>>=|\*\*=|[-.+*/%&|^]=|=(?!=|>))`, 'gu');
|
|
2089
|
+
return [...source.slice(start, end).matchAll(assignmentPattern)].some((match) => getPhpFileBraceDepth(source, start + (match.index ?? 0)) !== null);
|
|
2090
|
+
}
|
|
2091
|
+
function hasReachableBuildDirectoryReturn(bootstrapSource, getterRange) {
|
|
2092
|
+
// Support the direct form plus the generated fallback-candidate loop without
|
|
2093
|
+
// attempting to model arbitrary PHP data flow.
|
|
2094
|
+
// Each entry pairs a supported flow with its minimum PHP brace depth:
|
|
2095
|
+
// depth 1 is the function body; depth 2 is inside the generated foreach.
|
|
2096
|
+
const supportedReturns = [
|
|
2097
|
+
[
|
|
2098
|
+
new RegExp(String.raw `\breturn\s+${PHP_BUILD_DIRECTORY_EXPRESSION}\s*;`, 'gu'),
|
|
2099
|
+
1,
|
|
2100
|
+
],
|
|
2101
|
+
[
|
|
2102
|
+
new RegExp(String.raw `\$candidates\s*=\s*array\s*\(\s*${PHP_BUILD_DIRECTORY_EXPRESSION}(?:\s*,[\s\S]*?)?\)\s*;\s*foreach\s*\(\s*\$candidates\s+as\s+\$candidate\s*\)\s*\{[\s\S]*?\breturn\s+\$candidate\s*;`, 'gu'),
|
|
2103
|
+
2,
|
|
2104
|
+
],
|
|
2105
|
+
];
|
|
2106
|
+
return supportedReturns.some(([pattern, minimumReturnDepth]) => [...getterRange.source.matchAll(pattern)].some((match) => {
|
|
2107
|
+
const matchOffset = getterRange.start + (match.index ?? 0);
|
|
2108
|
+
const returnOffset = matchOffset + match[0].lastIndexOf('return');
|
|
2109
|
+
const foreachOffset = matchOffset + match[0].lastIndexOf('foreach');
|
|
2110
|
+
const candidateFlowWasReassigned = minimumReturnDepth > 1 &&
|
|
2111
|
+
(hasPhpVariableReassignment(bootstrapSource, 'candidates', matchOffset + match[0].indexOf('=') + 1, foreachOffset) ||
|
|
2112
|
+
hasPhpVariableReassignment(bootstrapSource, 'candidate', foreachOffset, returnOffset));
|
|
2113
|
+
return (getPhpFileBraceDepth(bootstrapSource, matchOffset) === 1 &&
|
|
2114
|
+
(getPhpFileBraceDepth(bootstrapSource, returnOffset) ?? 0) >=
|
|
2115
|
+
minimumReturnDepth &&
|
|
2116
|
+
!candidateFlowWasReassigned &&
|
|
2117
|
+
!hasEarlierDirectPhpCompletion(bootstrapSource, getterRange.start, returnOffset));
|
|
2118
|
+
}));
|
|
2119
|
+
}
|
|
2120
|
+
function hasDirectBuildDirectoryRegistration(bootstrapSource, callbackRange) {
|
|
2121
|
+
const buildDirectorySentinel = '__wp_typia_build_directory__';
|
|
2122
|
+
const callbackSource = callbackRange.source;
|
|
2123
|
+
if ([
|
|
2124
|
+
/\bfunction\s*(?:&\s*)?\(/gu,
|
|
2125
|
+
/\bfn\s*(?:&\s*)?\(/gu,
|
|
2126
|
+
].some((pattern) => [...callbackSource.matchAll(pattern)].some((match) => getPhpRangeMatchDepth(bootstrapSource, callbackRange, match) !== null))) {
|
|
2127
|
+
return false;
|
|
2128
|
+
}
|
|
2129
|
+
const assignmentPattern = /\$build_dir\s*=\s*([A-Za-z_][A-Za-z0-9_]*_get_build_dir)\s*\(\s*\)\s*;/gu;
|
|
2130
|
+
const assignment = [...callbackSource.matchAll(assignmentPattern)].find((match) => getPhpRangeMatchDepth(bootstrapSource, callbackRange, match) === 1);
|
|
2131
|
+
if (!assignment) {
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
const getterRange = findPhpFunctionRange(bootstrapSource, assignment[1], {
|
|
2135
|
+
requirePhpOpenTag: true,
|
|
2136
|
+
});
|
|
2137
|
+
if (!getterRange ||
|
|
2138
|
+
!hasReachableBuildDirectoryReturn(bootstrapSource, getterRange)) {
|
|
2139
|
+
return false;
|
|
2140
|
+
}
|
|
2141
|
+
const assignmentEndInRange = (assignment.index ?? 0) + assignment[0].length;
|
|
2142
|
+
const assignmentEnd = callbackRange.start + assignmentEndInRange;
|
|
2143
|
+
const sourceAfterAssignment = callbackSource.slice(assignmentEndInRange);
|
|
2144
|
+
if (hasPhpVariableReassignment(bootstrapSource, 'build_dir', assignmentEnd, callbackRange.end)) {
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
const registrationSource = bootstrapSource.slice(0, assignmentEnd) +
|
|
2148
|
+
bootstrapSource
|
|
2149
|
+
.slice(assignmentEnd, callbackRange.end)
|
|
2150
|
+
.replace(/\$build_dir\b/gu, `'${buildDirectorySentinel}'`) +
|
|
2151
|
+
bootstrapSource.slice(callbackRange.end);
|
|
2152
|
+
const hasDirectRegistration = [
|
|
2153
|
+
...sourceAfterAssignment.matchAll(/\bregister_block_type\s*\(\s*\$build_dir\b/gu),
|
|
2154
|
+
].some((match) => {
|
|
2155
|
+
// Relative to sourceAfterAssignment for the call-prefix check below.
|
|
2156
|
+
const matchIndex = match.index ?? 0;
|
|
2157
|
+
let previousIndex = matchIndex - 1;
|
|
2158
|
+
while (/\s/u.test(sourceAfterAssignment[previousIndex] ?? '')) {
|
|
2159
|
+
previousIndex -= 1;
|
|
2160
|
+
}
|
|
2161
|
+
// Absolute in bootstrapSource for PHP depth and reachability checks.
|
|
2162
|
+
const registrationOffset = assignmentEnd + matchIndex;
|
|
2163
|
+
return (getPhpFileBraceDepth(bootstrapSource, registrationOffset) === 1 &&
|
|
2164
|
+
!hasEarlierDirectPhpCompletion(bootstrapSource, assignmentEnd, registrationOffset) &&
|
|
2165
|
+
!((sourceAfterAssignment[previousIndex] === '>' &&
|
|
2166
|
+
sourceAfterAssignment[previousIndex - 1] === '-') ||
|
|
2167
|
+
(sourceAfterAssignment[previousIndex] === ':' &&
|
|
2168
|
+
sourceAfterAssignment[previousIndex - 1] === ':')));
|
|
2169
|
+
});
|
|
2170
|
+
return (hasDirectRegistration &&
|
|
2171
|
+
hasPhpFunctionCallWithStringArguments(registrationSource, 'register_block_type', [buildDirectorySentinel], { requirePhpOpenTag: true }));
|
|
2172
|
+
}
|
|
2173
|
+
function hasDirectRestRouteRegistration(bootstrapSource, callbackRange, expectedRegistrations) {
|
|
2174
|
+
if (expectedRegistrations.length === 0) {
|
|
2175
|
+
return false;
|
|
2176
|
+
}
|
|
2177
|
+
const matches = [
|
|
2178
|
+
...callbackRange.source.matchAll(/\bregister_rest_route\s*\(/gu),
|
|
2179
|
+
];
|
|
2180
|
+
const isInsideArrowFunction = (relativeOffset) => {
|
|
2181
|
+
let arrowStarted = false;
|
|
2182
|
+
let arrowBodyStarted = false;
|
|
2183
|
+
for (const token of callbackRange.source
|
|
2184
|
+
.slice(0, relativeOffset)
|
|
2185
|
+
.matchAll(/\bfn\b|=>|;/gu)) {
|
|
2186
|
+
if (getPhpRangeMatchDepth(bootstrapSource, callbackRange, token) !== 1) {
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
if (token[0] === ';') {
|
|
2190
|
+
arrowStarted = false;
|
|
2191
|
+
arrowBodyStarted = false;
|
|
2192
|
+
}
|
|
2193
|
+
else if (token[0] === 'fn') {
|
|
2194
|
+
arrowStarted = true;
|
|
2195
|
+
arrowBodyStarted = false;
|
|
2196
|
+
}
|
|
2197
|
+
else if (arrowStarted) {
|
|
2198
|
+
arrowBodyStarted = true;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return arrowBodyStarted;
|
|
2202
|
+
};
|
|
2203
|
+
const directCalls = matches.flatMap((match) => {
|
|
2204
|
+
const relativeOffset = match.index ?? 0;
|
|
2205
|
+
const registrationOffset = callbackRange.start + relativeOffset;
|
|
2206
|
+
const sourceBeforeCall = callbackRange.source.slice(0, relativeOffset);
|
|
2207
|
+
let previousIndex = relativeOffset - 1;
|
|
2208
|
+
while (/\s/u.test(callbackRange.source[previousIndex] ?? '')) {
|
|
2209
|
+
previousIndex -= 1;
|
|
2210
|
+
}
|
|
2211
|
+
if (getPhpRangeMatchDepth(bootstrapSource, callbackRange, match) === 1 &&
|
|
2212
|
+
!hasEarlierDirectPhpCompletion(bootstrapSource, callbackRange.start, registrationOffset) &&
|
|
2213
|
+
!isInsideArrowFunction(relativeOffset) &&
|
|
2214
|
+
!/\b(?:function|new)\s*&?\s*$/u.test(sourceBeforeCall) &&
|
|
2215
|
+
callbackRange.source[previousIndex] !== '$' &&
|
|
2216
|
+
!((callbackRange.source[previousIndex] === '>' &&
|
|
2217
|
+
callbackRange.source[previousIndex - 1] === '-') ||
|
|
2218
|
+
(callbackRange.source[previousIndex] === ':' &&
|
|
2219
|
+
callbackRange.source[previousIndex - 1] === ':'))) {
|
|
2220
|
+
const callEnd = findPhpFunctionCallEnd(callbackRange.source, relativeOffset, 'register_rest_route');
|
|
2221
|
+
if (callEnd === null) {
|
|
2222
|
+
return [];
|
|
2223
|
+
}
|
|
2224
|
+
return [
|
|
2225
|
+
{
|
|
2226
|
+
relativeStart: relativeOffset,
|
|
2227
|
+
source: callbackRange.source.slice(relativeOffset, callEnd),
|
|
2228
|
+
},
|
|
2229
|
+
];
|
|
2230
|
+
}
|
|
2231
|
+
return [];
|
|
2232
|
+
});
|
|
2233
|
+
return expectedRegistrations.every(({ method, path: endpointPath }) => {
|
|
2234
|
+
const pathSegments = endpointPath.split('/').filter(Boolean);
|
|
2235
|
+
if (pathSegments.length < 2) {
|
|
2236
|
+
return false;
|
|
2237
|
+
}
|
|
2238
|
+
const argumentPairs = pathSegments.slice(1).map((_, splitIndex) => [
|
|
2239
|
+
pathSegments.slice(0, splitIndex + 1).join('/'),
|
|
2240
|
+
`/${pathSegments.slice(splitIndex + 1).join('/')}`,
|
|
2241
|
+
]);
|
|
2242
|
+
const expectedMethodConstant = WORDPRESS_REST_METHOD_CONSTANTS[method];
|
|
2243
|
+
return directCalls.some((call) => {
|
|
2244
|
+
const hasEndpointPath = argumentPairs.some((argumentPair) =>
|
|
2245
|
+
// The exact call begins inside an already-open PHP callback, so snippet
|
|
2246
|
+
// scanning must start in PHP mode instead of requiring another tag.
|
|
2247
|
+
hasPhpFunctionCallWithStringArguments(call.source, 'register_rest_route', argumentPair));
|
|
2248
|
+
if (!hasEndpointPath) {
|
|
2249
|
+
return false;
|
|
2250
|
+
}
|
|
2251
|
+
return [
|
|
2252
|
+
...call.source.matchAll(/(?:'methods'|"methods")\s*=>\s*\\?WP_REST_Server::(READABLE|CREATABLE|EDITABLE|DELETABLE)\b/gu),
|
|
2253
|
+
].some((methodMatch) => {
|
|
2254
|
+
if (methodMatch[1] !== expectedMethodConstant) {
|
|
2255
|
+
return false;
|
|
2256
|
+
}
|
|
2257
|
+
const classOffset = methodMatch[0].lastIndexOf('WP_REST_Server');
|
|
2258
|
+
const relativeClassOffset = call.relativeStart + (methodMatch.index ?? 0) + classOffset;
|
|
2259
|
+
return (classOffset >= 0 &&
|
|
2260
|
+
!isInsideArrowFunction(relativeClassOffset) &&
|
|
2261
|
+
getPhpFileBraceDepth(bootstrapSource, callbackRange.start + relativeClassOffset) === 1);
|
|
2262
|
+
});
|
|
2263
|
+
});
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
function getBootstrapCheck(project, parsedRestConfig) {
|
|
2267
|
+
const { requiresRest } = parsedRestConfig;
|
|
2268
|
+
const expectedRestRegistrations = parsedRestConfig.manifest?.endpoints.map(({ method, path: endpointPath }) => ({
|
|
2269
|
+
method,
|
|
2270
|
+
path: endpointPath,
|
|
2271
|
+
})) ?? [];
|
|
2272
|
+
const packageBaseName = getSafePackageBaseName(project.packageName);
|
|
2273
|
+
if (!packageBaseName) {
|
|
2274
|
+
return createDoctorCheck('Standalone plugin bootstrap', 'fail', 'Package name cannot derive a safe project-local plugin bootstrap path', STANDALONE_DOCTOR_CODES.BOOTSTRAP);
|
|
2275
|
+
}
|
|
2276
|
+
const bootstrapRelativePath = `${packageBaseName}.php`;
|
|
2277
|
+
const bootstrapPath = path.join(project.projectDir, bootstrapRelativePath);
|
|
2278
|
+
if (!fs.existsSync(bootstrapPath)) {
|
|
2279
|
+
return createDoctorCheck('Standalone plugin bootstrap', 'fail', `Missing package-aligned plugin bootstrap ${bootstrapRelativePath}`, STANDALONE_DOCTOR_CODES.BOOTSTRAP);
|
|
2280
|
+
}
|
|
2281
|
+
let source;
|
|
2282
|
+
let headerRegion;
|
|
2283
|
+
try {
|
|
2284
|
+
const sourceBuffer = fs.readFileSync(bootstrapPath);
|
|
2285
|
+
source = sourceBuffer.toString('utf8');
|
|
2286
|
+
headerRegion = sourceBuffer
|
|
2287
|
+
.subarray(0, WORDPRESS_PLUGIN_HEADER_SCAN_BYTES)
|
|
2288
|
+
.toString('utf8')
|
|
2289
|
+
.replace(/^\uFEFF/u, '');
|
|
2290
|
+
}
|
|
2291
|
+
catch {
|
|
2292
|
+
return createDoctorCheck('Standalone plugin bootstrap', 'fail', `Unable to read package-aligned plugin bootstrap ${bootstrapRelativePath}`, STANDALONE_DOCTOR_CODES.BOOTSTRAP);
|
|
2293
|
+
}
|
|
2294
|
+
const hasPluginHeader = WORDPRESS_PLUGIN_NAME_HEADER_PATTERN.test(headerRegion);
|
|
2295
|
+
const hasRegistrationCall = hasPhpFunctionCall(source, 'register_block_type', { requirePhpOpenTag: true });
|
|
2296
|
+
const hasRegistrationHook = hasPhpFunctionCallWithStringArguments(source, 'add_action', [
|
|
2297
|
+
'init',
|
|
2298
|
+
(callbackName) => {
|
|
2299
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*_register_block$/u.test(callbackName)) {
|
|
2300
|
+
return false;
|
|
2301
|
+
}
|
|
2302
|
+
const callbackRange = findPhpFunctionRange(source, callbackName, {
|
|
2303
|
+
requirePhpOpenTag: true,
|
|
2304
|
+
});
|
|
2305
|
+
return (callbackRange !== null &&
|
|
2306
|
+
hasDirectBuildDirectoryRegistration(source, callbackRange));
|
|
2307
|
+
},
|
|
2308
|
+
], { requirePhpOpenTag: true });
|
|
2309
|
+
const hasRestRegistrationCall = !requiresRest ||
|
|
2310
|
+
hasPhpFunctionCall(source, 'register_rest_route', {
|
|
2311
|
+
requirePhpOpenTag: true,
|
|
2312
|
+
});
|
|
2313
|
+
const hasRestRegistrationHook = !requiresRest ||
|
|
2314
|
+
hasPhpFunctionCallWithStringArguments(source, 'add_action', [
|
|
2315
|
+
'rest_api_init',
|
|
2316
|
+
(callbackName) => {
|
|
2317
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*_register_routes$/u.test(callbackName)) {
|
|
2318
|
+
return false;
|
|
2319
|
+
}
|
|
2320
|
+
const callbackRange = findPhpFunctionRange(source, callbackName, {
|
|
2321
|
+
requirePhpOpenTag: true,
|
|
2322
|
+
});
|
|
2323
|
+
return (callbackRange !== null &&
|
|
2324
|
+
hasDirectRestRouteRegistration(source, callbackRange, expectedRestRegistrations));
|
|
2325
|
+
},
|
|
2326
|
+
], { requirePhpOpenTag: true });
|
|
2327
|
+
const issues = [
|
|
2328
|
+
...(!hasPluginHeader ? ['is missing a Plugin Name header'] : []),
|
|
2329
|
+
...(!hasRegistrationCall ? ['does not call register_block_type()'] : []),
|
|
2330
|
+
...(!hasRegistrationHook
|
|
2331
|
+
? ['does not hook block registration to init']
|
|
2332
|
+
: []),
|
|
2333
|
+
...(!hasRestRegistrationCall
|
|
2334
|
+
? ['does not call register_rest_route()']
|
|
2335
|
+
: []),
|
|
2336
|
+
...(!hasRestRegistrationHook
|
|
2337
|
+
? ['does not hook REST route registration to rest_api_init']
|
|
2338
|
+
: []),
|
|
2339
|
+
];
|
|
2340
|
+
return createDoctorCheck('Standalone plugin bootstrap', issues.length === 0 ? 'pass' : 'fail', issues.length === 0
|
|
2341
|
+
? `${bootstrapRelativePath} contains a WordPress plugin header and ${requiresRest
|
|
2342
|
+
? 'init block registration plus REST route wiring'
|
|
2343
|
+
: 'init registration wiring'}`
|
|
2344
|
+
: `${bootstrapRelativePath} ${issues.join('; ')}`, STANDALONE_DOCTOR_CODES.BOOTSTRAP);
|
|
2345
|
+
}
|
|
2346
|
+
function getSourceLayoutCheck(project, parsedConfig, parsedRestConfig, requiresInteractivity) {
|
|
2347
|
+
const packageBaseName = getSafePackageBaseName(project.packageName);
|
|
2348
|
+
const bootstrapPath = packageBaseName
|
|
2349
|
+
? path.join(project.projectDir, `${packageBaseName}.php`)
|
|
2350
|
+
: null;
|
|
2351
|
+
const bootstrapLocalIncludes = [];
|
|
2352
|
+
if (bootstrapPath && fs.existsSync(bootstrapPath)) {
|
|
2353
|
+
try {
|
|
2354
|
+
const bootstrapSource = fs.readFileSync(bootstrapPath, 'utf8');
|
|
2355
|
+
for (const match of bootstrapSource.matchAll(/\brequire_once\s+__DIR__\s*\.\s*(['"])\/([^'"]+)\1\s*;/gu)) {
|
|
2356
|
+
const relativePath = match[2];
|
|
2357
|
+
if (relativePath &&
|
|
2358
|
+
getPhpFileBraceDepth(bootstrapSource, match.index ?? 0) !== null &&
|
|
2359
|
+
isSafeProjectRelativePath(project.projectDir, relativePath)) {
|
|
2360
|
+
bootstrapLocalIncludes.push(path.normalize(relativePath));
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
catch {
|
|
2365
|
+
// The bootstrap row reports unreadable plugin files directly.
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
let featureSourceFiles = STANDALONE_BASIC_SOURCE_FILES;
|
|
2369
|
+
if (parsedRestConfig.requiresRest) {
|
|
2370
|
+
featureSourceFiles = STANDALONE_PERSISTENCE_SOURCE_FILES;
|
|
2371
|
+
}
|
|
2372
|
+
else if (requiresInteractivity) {
|
|
2373
|
+
featureSourceFiles = STANDALONE_INTERACTIVITY_SOURCE_FILES;
|
|
2374
|
+
}
|
|
2375
|
+
const requiredFiles = new Set([
|
|
2376
|
+
...STANDALONE_COMMON_SOURCE_FILES,
|
|
2377
|
+
...featureSourceFiles,
|
|
2378
|
+
...bootstrapLocalIncludes,
|
|
2379
|
+
]);
|
|
2380
|
+
const missingFiles = [...requiredFiles].filter((relativePath) => !fs.existsSync(path.join(project.projectDir, relativePath)));
|
|
2381
|
+
const syncProjectProblem = getSyncProjectDelegationProblem(project, parsedRestConfig.requiresRest);
|
|
2382
|
+
const issues = [
|
|
2383
|
+
...(missingFiles.length > 0 ? [`Missing: ${missingFiles.join(', ')}`] : []),
|
|
2384
|
+
...(parsedConfig.problem ? [parsedConfig.problem] : []),
|
|
2385
|
+
...(parsedRestConfig.problem ? [parsedRestConfig.problem] : []),
|
|
2386
|
+
...(syncProjectProblem ? [syncProjectProblem] : []),
|
|
2387
|
+
];
|
|
2388
|
+
return createDoctorCheck('Standalone source layout', issues.length === 0 ? 'pass' : 'fail', issues.length === 0
|
|
2389
|
+
? 'Supported standalone source surface and static canonical sync configuration detected'
|
|
2390
|
+
: issues.join('; '), STANDALONE_DOCTOR_CODES.SOURCE_LAYOUT);
|
|
2391
|
+
}
|
|
2392
|
+
function resolveFromProject(projectDir, packageName, resolutionSpecifier) {
|
|
2393
|
+
const projectRequire = createRequire(path.join(projectDir, 'package.json'));
|
|
2394
|
+
try {
|
|
2395
|
+
const resolvedPath = projectRequire.resolve(resolutionSpecifier);
|
|
2396
|
+
const pnpVersion = process.versions.pnp;
|
|
2397
|
+
if (typeof pnpVersion === 'number' ||
|
|
2398
|
+
(typeof pnpVersion === 'string' && pnpVersion.length > 0)) {
|
|
2399
|
+
const pnpApi = projectRequire('pnpapi');
|
|
2400
|
+
const issuerLocator = pnpApi.findPackageLocator(path.join(projectDir, 'package.json'));
|
|
2401
|
+
const resolvedLocator = pnpApi.findPackageLocator(resolvedPath);
|
|
2402
|
+
if (!issuerLocator || !resolvedLocator) {
|
|
2403
|
+
return null;
|
|
2404
|
+
}
|
|
2405
|
+
const issuerInformation = pnpApi.getPackageInformation(issuerLocator);
|
|
2406
|
+
if (!issuerInformation) {
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
const dependencyReference = issuerInformation.packageDependencies.get(packageName);
|
|
2410
|
+
if (dependencyReference === undefined || dependencyReference === null) {
|
|
2411
|
+
return null;
|
|
2412
|
+
}
|
|
2413
|
+
const expectedLocator = pnpApi.getLocator(packageName, dependencyReference);
|
|
2414
|
+
// Keep the virtual path so Yarn can retain its peer-dependency locator.
|
|
2415
|
+
return resolvedLocator.name === expectedLocator.name &&
|
|
2416
|
+
resolvedLocator.reference === expectedLocator.reference
|
|
2417
|
+
? resolvedPath
|
|
2418
|
+
: null;
|
|
2419
|
+
}
|
|
2420
|
+
const localPackageEntry = path.join(projectDir, 'node_modules', ...packageName.split('/'));
|
|
2421
|
+
if (!fs.existsSync(localPackageEntry)) {
|
|
2422
|
+
return null;
|
|
2423
|
+
}
|
|
2424
|
+
const localPackageRoot = fs.realpathSync(localPackageEntry);
|
|
2425
|
+
const realResolvedPath = fs.realpathSync(resolvedPath);
|
|
2426
|
+
return isProjectLocalRelativePath(path.relative(localPackageRoot, realResolvedPath))
|
|
2427
|
+
? realResolvedPath
|
|
2428
|
+
: null;
|
|
2429
|
+
}
|
|
2430
|
+
catch {
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function canResolveFromProject(projectDir, packageName, resolutionSpecifier) {
|
|
2435
|
+
return (resolveFromProject(projectDir, packageName, resolutionSpecifier) !== null);
|
|
2436
|
+
}
|
|
2437
|
+
async function loadProjectMetadataCore(project, requiresRest) {
|
|
2438
|
+
const modulePath = resolveFromProject(project.projectDir, '@wp-typia/block-runtime', '@wp-typia/block-runtime/metadata-core');
|
|
2439
|
+
if (!modulePath) {
|
|
2440
|
+
throw new Error('Unable to resolve project-local @wp-typia/block-runtime/metadata-core.');
|
|
2441
|
+
}
|
|
2442
|
+
const loaded = (await import(pathToFileURL(modulePath).href));
|
|
2443
|
+
const requiredExports = [
|
|
2444
|
+
'resolveSyncBlockMetadataPaths',
|
|
2445
|
+
'runSyncBlockMetadata',
|
|
2446
|
+
...(requiresRest
|
|
2447
|
+
? [
|
|
2448
|
+
'defineEndpointManifest',
|
|
2449
|
+
'syncEndpointClient',
|
|
2450
|
+
'syncRestOpenApi',
|
|
2451
|
+
'syncTypeSchemas',
|
|
2452
|
+
]
|
|
2453
|
+
: []),
|
|
2454
|
+
];
|
|
2455
|
+
const missingExport = requiredExports.find((exportName) => typeof loaded[exportName] !== 'function');
|
|
2456
|
+
if (missingExport) {
|
|
2457
|
+
throw new Error(`Project-local metadata-core does not export ${missingExport}().`);
|
|
2458
|
+
}
|
|
2459
|
+
return loaded;
|
|
2460
|
+
}
|
|
2461
|
+
function getDependenciesCheck(project, requiresRest, requiresInteractivity) {
|
|
2462
|
+
const requiredInstalledPackages = [
|
|
2463
|
+
...REQUIRED_INSTALLED_PACKAGES,
|
|
2464
|
+
...(requiresInteractivity
|
|
2465
|
+
? REQUIRED_INTERACTIVITY_INSTALLED_PACKAGES
|
|
2466
|
+
: []),
|
|
2467
|
+
...(requiresRest ? REQUIRED_REST_INSTALLED_PACKAGES : []),
|
|
2468
|
+
];
|
|
2469
|
+
const missingPackages = requiredInstalledPackages
|
|
2470
|
+
.filter(({ packageName, resolutionSpecifier }) => !canResolveFromProject(project.projectDir, packageName, resolutionSpecifier))
|
|
2471
|
+
.map(({ diagnosticName }) => diagnosticName);
|
|
2472
|
+
const packageManager = inferPackageManagerId(project.projectDir, getPackageManagerSelector(project));
|
|
2473
|
+
return createDoctorCheck('Standalone dependencies', missingPackages.length === 0 ? 'pass' : 'fail', missingPackages.length === 0
|
|
2474
|
+
? 'Project-local wp-typia runtime, TypeScript, and script-runner dependencies are installed'
|
|
2475
|
+
: `Missing installed package(s): ${missingPackages.join(', ')}. Run \`${formatInstallCommand(packageManager)}\` from the standalone project root.`, STANDALONE_DOCTOR_CODES.DEPENDENCIES);
|
|
2476
|
+
}
|
|
2477
|
+
function toProjectRelativePath(projectDir, filePath) {
|
|
2478
|
+
const relativePath = path.relative(projectDir, filePath);
|
|
2479
|
+
return isProjectLocalRelativePath(relativePath)
|
|
2480
|
+
? relativePath.split(path.sep).join('/')
|
|
2481
|
+
: path.basename(filePath);
|
|
2482
|
+
}
|
|
2483
|
+
function escapeRegExp(value) {
|
|
2484
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
2485
|
+
}
|
|
2486
|
+
function sanitizeProjectPaths(message, projectDir) {
|
|
2487
|
+
const resolvedProjectDir = path.resolve(projectDir);
|
|
2488
|
+
const pathVariants = new Set([
|
|
2489
|
+
resolvedProjectDir,
|
|
2490
|
+
resolvedProjectDir.split(path.sep).join('/'),
|
|
2491
|
+
resolvedProjectDir.split(path.sep).join('\\'),
|
|
2492
|
+
]);
|
|
2493
|
+
let sanitizedMessage = message;
|
|
2494
|
+
for (const pathVariant of pathVariants) {
|
|
2495
|
+
sanitizedMessage = sanitizedMessage.replace(new RegExp(`${escapeRegExp(pathVariant)}(?=[/\\\\]|$)`, 'giu'), '.');
|
|
2496
|
+
}
|
|
2497
|
+
return sanitizedMessage;
|
|
2498
|
+
}
|
|
2499
|
+
function getConfiguredArtifactPaths(options, metadataCore) {
|
|
2500
|
+
const resolvedPaths = (metadataCore?.resolveSyncBlockMetadataPaths ?? resolveSyncBlockMetadataPaths)(options);
|
|
2501
|
+
return [
|
|
2502
|
+
resolvedPaths.blockJsonPath,
|
|
2503
|
+
resolvedPaths.manifestPath,
|
|
2504
|
+
resolvedPaths.phpValidatorPath,
|
|
2505
|
+
...(resolvedPaths.jsonSchemaPath ? [resolvedPaths.jsonSchemaPath] : []),
|
|
2506
|
+
...(resolvedPaths.openApiPath ? [resolvedPaths.openApiPath] : []),
|
|
2507
|
+
];
|
|
2508
|
+
}
|
|
2509
|
+
function formatSyncFailure(project, report) {
|
|
2510
|
+
const failure = report.failure;
|
|
2511
|
+
if (!failure) {
|
|
2512
|
+
return 'Canonical sync check failed without a structured failure.';
|
|
2513
|
+
}
|
|
2514
|
+
const normalizedMessage = sanitizeProjectPaths(failure.message, project.projectDir);
|
|
2515
|
+
return `Canonical sync check failed (${failure.code}): ${normalizedMessage}`;
|
|
2516
|
+
}
|
|
2517
|
+
async function getGeneratedArtifactsCheck(project, parsedConfig, parsedRestConfig, dependenciesReady) {
|
|
2518
|
+
const packageManager = inferPackageManagerId(project.projectDir, getPackageManagerSelector(project));
|
|
2519
|
+
const syncCommand = formatRunScript(packageManager, 'sync');
|
|
2520
|
+
if (!parsedConfig.options || parsedRestConfig.problem) {
|
|
2521
|
+
return createDoctorCheck('Standalone generated artifacts', 'fail', `Canonical freshness check is blocked by the source layout. Fix that row, run \`${syncCommand}\`, and rerun doctor.`, STANDALONE_DOCTOR_CODES.ARTIFACTS);
|
|
2522
|
+
}
|
|
2523
|
+
const freshnessOptions = parsedRestConfig.manifest
|
|
2524
|
+
? {
|
|
2525
|
+
...parsedConfig.options,
|
|
2526
|
+
...STANDALONE_PERSISTENCE_BLOCK_SCHEMA_ARTIFACTS,
|
|
2527
|
+
}
|
|
2528
|
+
: parsedConfig.options;
|
|
2529
|
+
let artifactPaths = [
|
|
2530
|
+
...getConfiguredArtifactPaths(freshnessOptions),
|
|
2531
|
+
...parsedRestConfig.artifactPaths,
|
|
2532
|
+
];
|
|
2533
|
+
const missingArtifacts = artifactPaths.filter((artifactPath) => !fs.existsSync(artifactPath));
|
|
2534
|
+
if (!dependenciesReady) {
|
|
2535
|
+
return createDoctorCheck('Standalone generated artifacts', missingArtifacts.length > 0 ? 'fail' : 'warn', missingArtifacts.length > 0
|
|
2536
|
+
? `Missing: ${missingArtifacts
|
|
2537
|
+
.map((filePath) => toProjectRelativePath(project.projectDir, filePath))
|
|
2538
|
+
.join(', ')}. Install dependencies, run \`${syncCommand}\`, and rerun doctor.`
|
|
2539
|
+
: 'Expected generated files exist, but canonical freshness could not be checked until project dependencies are installed.', STANDALONE_DOCTOR_CODES.ARTIFACTS);
|
|
2540
|
+
}
|
|
2541
|
+
let metadataCore;
|
|
2542
|
+
let report;
|
|
2543
|
+
try {
|
|
2544
|
+
metadataCore = await loadProjectMetadataCore(project, parsedRestConfig.requiresRest);
|
|
2545
|
+
artifactPaths = [
|
|
2546
|
+
...getConfiguredArtifactPaths(freshnessOptions, metadataCore),
|
|
2547
|
+
...parsedRestConfig.artifactPaths,
|
|
2548
|
+
];
|
|
2549
|
+
report = await metadataCore.runSyncBlockMetadata(freshnessOptions, {
|
|
2550
|
+
check: true,
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
catch (error) {
|
|
2554
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2555
|
+
const normalizedMessage = sanitizeProjectPaths(message, project.projectDir);
|
|
2556
|
+
return createDoctorCheck('Standalone generated artifacts', 'fail', `Canonical sync check encountered an unexpected error: ${normalizedMessage}. Run \`${syncCommand}\` and rerun doctor.`, STANDALONE_DOCTOR_CODES.ARTIFACTS);
|
|
2557
|
+
}
|
|
2558
|
+
if (report.failure || report.status === 'error') {
|
|
2559
|
+
return createDoctorCheck('Standalone generated artifacts', 'fail', `${formatSyncFailure(project, report)} Run \`${syncCommand}\` and rerun doctor.`, STANDALONE_DOCTOR_CODES.ARTIFACTS);
|
|
2560
|
+
}
|
|
2561
|
+
if (parsedRestConfig.manifest) {
|
|
2562
|
+
try {
|
|
2563
|
+
await checkStandaloneRestArtifacts(project.projectDir, parsedRestConfig, metadataCore);
|
|
2564
|
+
}
|
|
2565
|
+
catch (error) {
|
|
2566
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2567
|
+
const normalizedMessage = sanitizeProjectPaths(message, project.projectDir);
|
|
2568
|
+
return createDoctorCheck('Standalone generated artifacts', 'fail', `Canonical REST sync check failed: ${normalizedMessage}. Run \`${syncCommand}\` and rerun doctor.`, STANDALONE_DOCTOR_CODES.ARTIFACTS);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
const warningCount = report.lossyProjectionWarnings.length + report.phpGenerationWarnings.length;
|
|
2572
|
+
const artifactSummary = artifactPaths
|
|
2573
|
+
.map((filePath) => toProjectRelativePath(project.projectDir, filePath))
|
|
2574
|
+
.join(', ');
|
|
2575
|
+
return createDoctorCheck('Standalone generated artifacts', 'pass', warningCount > 0
|
|
2576
|
+
? `Canonical artifacts are current: ${artifactSummary}. The sync report also contains ${warningCount} projection notice(s); use the sync-types strict flags when those notices should enforce policy.`
|
|
2577
|
+
: `Canonical artifacts are current: ${artifactSummary}`, STANDALONE_DOCTOR_CODES.ARTIFACTS);
|
|
2578
|
+
}
|
|
2579
|
+
/** Collect project-scoped checks for one supported standalone scaffold. */
|
|
2580
|
+
export async function getStandaloneScaffoldDoctorChecks(project) {
|
|
2581
|
+
const parsedConfig = parseStandaloneSyncConfig(project);
|
|
2582
|
+
const requiresRest = standaloneProjectRequiresRest(project.projectDir, project.packageJson);
|
|
2583
|
+
const requiresInteractivity = standaloneProjectRequiresInteractivity(project, requiresRest);
|
|
2584
|
+
const parsedRestConfig = parseStandaloneRestConfig(project.projectDir, requiresRest, parsedConfig.options?.sourceTypeName ?? null);
|
|
2585
|
+
const dependenciesCheck = getDependenciesCheck(project, requiresRest, requiresInteractivity);
|
|
2586
|
+
return [
|
|
2587
|
+
createDoctorScopeCheck('pass', `Scope: standalone scaffold diagnostics for ${project.packageName}. Environment readiness checks ran and package metadata, plugin bootstrap, source layout, dependencies, and canonical generated artifacts are checked below.`),
|
|
2588
|
+
getPackageMetadataCheck(project, requiresRest, requiresInteractivity),
|
|
2589
|
+
getBootstrapCheck(project, parsedRestConfig),
|
|
2590
|
+
getSourceLayoutCheck(project, parsedConfig, parsedRestConfig, requiresInteractivity),
|
|
2591
|
+
dependenciesCheck,
|
|
2592
|
+
await getGeneratedArtifactsCheck(project, parsedConfig, parsedRestConfig, dependenciesCheck.status === 'pass'),
|
|
2593
|
+
];
|
|
2594
|
+
}
|