Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/strands/p5.strands.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,18 @@ if (typeof p5 !== "undefined") {
*/

/**
* @property {Object} filterColor
* @typedef {Object} FilterColorHook
* @property {any} texCoord
* @property {any} canvasSize
* @property {any} texelSize
* @property {any} canvasContent
* @property {function(): undefined} begin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be : void too or is there a reason for them to be : undefined?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I tried using void, the generated p5.d.ts ended up showing any instead. I tried using different ways to define a function but it still kept showing any. So I've defined it as undefined which showed up correctly while generating p5.js.d.ts

* @property {function(): undefined} end
* @property {function(color: any): void} set
*/

/**
* @property {FilterColorHook} filterColor
* @description
* A shader hook block that sets the color for each pixel in a filter shader. This hook can be used inside <a href="#/p5/buildFilterShader">`buildFilterShader()`</a> to control the output color for each pixel.
*
Expand Down
1 change: 0 additions & 1 deletion utils/patch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,3 @@ export function applyPatches() {
}
}
}

141 changes: 135 additions & 6 deletions utils/typescript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ allRawData.forEach(entry => {
if (entry.kind === 'constant' || entry.kind === 'typedef') {
constantsLookup.add(entry.name);
if (entry.kind === 'typedef') {
typedefs[entry.name] = entry.type;
// Store the full entry so we have access to both .type and .properties
typedefs[entry.name] = entry;
}
}
});
Expand Down Expand Up @@ -242,15 +243,29 @@ function convertTypeToTypeScript(typeNode, options = {}) {
}
}

// Check if this is a p5 constant - use typeof since they're defined as values
// Check if this is a p5 constant/typedef
if (constantsLookup.has(typeName)) {
const typedefEntry = typedefs[typeName];

// Use interface name for object typedefs unless resolving the constant definition itself
if (typedefEntry && hasTypedefProperties(typedefEntry) && !isConstantDef) {
if (inGlobalMode) {
return `P5.${typeName}`;
} else if (isInsideNamespace) {
return typeName;
} else {
return `p5.${typeName}`;
}
}

// Fallback to typeof or primitive resolution for alias-style typedefs
if (inGlobalMode) {
return `typeof P5.${typeName}`;
} else if (typedefs[typeName]) {
} else if (typedefEntry) {
if (isConstantDef) {
return convertTypeToTypeScript(typedefs[typeName], options);
return convertTypeToTypeScript(typedefEntry.type, options);
} else {
return `typeof p5.${typeName}`
return `typeof p5.${typeName}`;
}
} else {
return `Symbol`;
Expand Down Expand Up @@ -330,6 +345,105 @@ function convertTypeToTypeScript(typeNode, options = {}) {
}
}

// Check if typedef represents a real object shape
function hasTypedefProperties(typedefEntry) {
if (!Array.isArray(typedefEntry.properties) || typedefEntry.properties.length === 0) {
return false;
}
// Reject self-referential single-property typedefs
if (
typedefEntry.properties.length === 1 &&
typedefEntry.properties[0].name === typedefEntry.name
) {
return false;
}
return true;
}

// Convert JSDoc FunctionType into a TypeScript function signature string
function convertFunctionTypeForInterface(typeNode, options) {
const params = (typeNode.params || [])
.map((param, i) => {
let typeObj;
let paramName;
if (param.type === 'ParameterType') {
typeObj = param.expression;
paramName = param.name ?? `p${i}`;
} else if (typeof param.type === 'object' && param.type !== null) {
typeObj = param.type;
paramName = param.name ?? `p${i}`;
} else {
// param itself is a plain type node
typeObj = param;
paramName = `p${i}`;
}
const paramType = convertTypeToTypeScript(typeObj, options);
return `${paramName}: ${paramType}`;
})
.join(', ');

const returnType = typeNode.result
? convertTypeToTypeScript(typeNode.result, options)
: 'void';

// Normalise 'undefined' return to 'void' for idiomatic TypeScript
const normalisedReturn = returnType === 'undefined' ? 'void' : returnType;

return `(${params}) => ${normalisedReturn}`;
}

// Generate a TypeScript interface from a typedef with @property fields
function generateTypedefInterface(name, typedefEntry, options = {}, indent = 2) {
const pad = ' '.repeat(indent);
const innerPad = ' '.repeat(indent + 2);
let output = '';

if (typedefEntry.description) {
const descStr = typeof typedefEntry.description === 'string'
? typedefEntry.description
: descriptionStringForTypeScript(typedefEntry.description);
if (descStr) {
output += `${pad}/**\n`;
output += formatJSDocComment(descStr, indent) + '\n';
output += `${pad} */\n`;
}
}

output += `${pad}interface ${name} {\n`;

for (const prop of typedefEntry.properties) {
// Each prop: { name, type, description, optional }
const propName = prop.name;
const rawType = prop.type;
const isOptional = prop.optional || rawType?.type === 'OptionalType';
const optMark = isOptional ? '?' : '';

if (prop.description) {
const propDescStr = typeof prop.description === 'string'
? prop.description.trim()
: descriptionStringForTypeScript(prop.description);
if (propDescStr) {
output += `${innerPad}/** ${propDescStr} */\n`;
}
}

if (rawType?.type === 'FunctionType') {
// Render FunctionType properties as method signatures instead of arrow properties
const sig = convertFunctionTypeForInterface(rawType, options);
const arrowIdx = sig.lastIndexOf('=>');
const paramsPart = sig.substring(0, arrowIdx).trim();
const retPart = sig.substring(arrowIdx + 2).trim();
output += `${innerPad}${propName}${paramsPart}: ${retPart};\n`;
} else {
const tsType = rawType ? convertTypeToTypeScript(rawType, options) : 'any';
output += `${innerPad}${propName}${optMark}: ${tsType};\n`;
}
}

output += `${pad}}\n\n`;
return output;
}

// Strategy for TypeScript output
const typescriptStrategy = {
shouldSkipEntry: (entry, context) => {
Expand Down Expand Up @@ -667,13 +781,20 @@ function generateTypeDefinitions() {

output += '\n';


p5Constants.forEach(constant => {
output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`;
});

output += '\n';

// Emit interfaces for typedefs that define object shapes
const namespaceOptions = { isInsideNamespace: true };
for (const [name, typedefEntry] of Object.entries(typedefs)) {
if (hasTypedefProperties(typedefEntry)) {
output += generateTypedefInterface(name, typedefEntry, namespaceOptions, 2);
}
}

// Generate other classes in namespace
Object.values(processed.classes).forEach(classData => {
if (classData.name !== 'p5') {
Expand Down Expand Up @@ -750,6 +871,14 @@ p5: P5;

globalDefinitions += '\n';

// Mirror typedef interfaces for global-mode usage
const globalNamespaceOptions = { isInsideNamespace: true, inGlobalMode: true };
for (const [name, typedefEntry] of Object.entries(typedefs)) {
if (hasTypedefProperties(typedefEntry)) {
globalDefinitions += generateTypedefInterface(name, typedefEntry, globalNamespaceOptions, 2);
}
}

// Add all real classes as both types and constructors
Object.values(processed.classes).forEach(classData => {
if (classData.name !== 'p5') {
Expand Down