Skip to content
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() {
}
}
}

99 changes: 80 additions & 19 deletions utils/typescript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@ const constantsLookup = new Set();
const typedefs = {};
const mutableProperties = new Set(['disableFriendlyErrors']); // Properties that should be mutable, not constants
allRawData.forEach(entry => {
if (entry.kind === 'constant' || entry.kind === 'typedef') {
if (entry.kind === 'constant') {
constantsLookup.add(entry.name);
if (entry.kind === 'typedef') {
typedefs[entry.name] = entry.type;
}

// Collect object typedefs so constants referencing them can resolve to proper types
if (entry.kind === 'typedef') {
if (
entry.properties &&
entry.properties.length > 0 &&
!entry.properties.every(p => p.name === entry.name)
) {
typedefs[entry.name] = entry;
}
}
});
Expand Down Expand Up @@ -201,6 +209,16 @@ function convertTypeToTypeScript(typeNode, options = {}) {
throw new Error(`convertTypeToTypeScript expects an object, got: ${typeof typeNode} - ${JSON.stringify(typeNode)}`);
}

if (typeNode.properties && typeNode.properties.length > 0) {
const props = typeNode.properties.map(prop => {
const propType = convertTypeToTypeScript(prop.type, options);
const optional = prop.optional ? '?' : '';
return `${prop.name}${optional}: ${propType}`;
});

return `{ ${props.join('; ')} }`;
}

const { currentClass = null, isInsideNamespace = false, inGlobalMode = false, isConstantDef = false } = options;

switch (typeNode.type) {
Expand Down Expand Up @@ -242,19 +260,18 @@ function convertTypeToTypeScript(typeNode, options = {}) {
}
}

// Check if this is a p5 constant - use typeof since they're defined as values
// If p5 constant: use its typedef when defining it, else reference it as a value via `typeof`
if (constantsLookup.has(typeName)) {
if (inGlobalMode) {
return `typeof P5.${typeName}`;
} else if (typedefs[typeName]) {
if (isConstantDef) {
return convertTypeToTypeScript(typedefs[typeName], options);
} else {
return `typeof p5.${typeName}`
}
} else {
return `Symbol`;
if (isConstantDef && typedefs[typeName]) {
return convertTypeToTypeScript(typedefs[typeName], options);
}
return inGlobalMode
? `typeof P5.${typeName}`
: `typeof ${typeName}`;
}

if (typedefs[typeName]) {
return typeName;
}

return typeName;
Expand Down Expand Up @@ -368,6 +385,11 @@ const typescriptStrategy = {

const processed = processData(rawData, typescriptStrategy);

// Augment constantsLookup with processed constants
Object.keys(processed.consts).forEach(name => {
constantsLookup.add(name);
});

function normalizeIdentifier(name) {
return (
'0123456789'.includes(name[0]) ||
Expand Down Expand Up @@ -595,6 +617,22 @@ function generateClassDeclaration(classData) {
function generateTypeDefinitions() {
let output = '// This file is auto-generated from JSDoc documentation\n\n';

const strandsMethods = processStrandsFunctions();
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason why this was moved up here from below?

Copy link
Author

Choose a reason for hiding this comment

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

I had moved it earlier because it was being used too late in the generation flow and wasn’t available in time, which was causing some errors. But after the recent changes I made, I don't think it's needed to initialise it at the start of the function


Object.entries(typedefs).forEach(([name, entry]) => {
if (entry.properties && entry.properties.length > 0) {
const props = entry.properties.map(prop => {
const propType = prop.type ? convertTypeToTypeScript(prop.type) : 'any';
const optional = prop.optional ? '?' : '';
return ` ${prop.name}${optional}: ${propType}`;
});
output += `type ${name} = {\n${props.join(';\n')};\n};\n\n`;
} else {
const tsType = convertTypeToTypeScript(entry.type || entry);
output += `type ${name} = ${tsType};\n\n`;
}
});

// First, define all constants at the top level with their actual values
const seenConstants = new Set();
const p5Constants = processed.classitems.filter(item => {
Expand All @@ -606,6 +644,9 @@ function generateTypeDefinitions() {
if (seenConstants.has(item.name)) {
return false;
}
if (item.name in typedefs) {
return false;
}
seenConstants.add(item.name);
return true;
}
Expand All @@ -618,12 +659,26 @@ function generateTypeDefinitions() {
output += formatJSDocComment(constant.description, 0) + '\n';
output += ' */\n';
}
const type = convertTypeToTypeScript(constant.type, { isInsideNamespace: false, isConstantDef: true });
let type;
// Avoid invalid self-referential types like `typeof FOO`
if (
constant.type?.type === 'NameExpression' &&
constant.type.name === constant.name
) {
// Self-referential constant → fallback
type = 'number';
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is not sufficient yet. When I run npm run generate-types, and then open types/p5.d.ts, I see this:

declare const P2D: number;

declare const __P2D: typeof P2D;

Previously, it looked like this:

declare const P2D: 'p2d';

declare const __P2D: typeof P2D;

I think a next step should be not just to get the tests to pass -- first, I would git checkout dev-2.0, run npm run generate-types, and then make a copy of types/p5.d.ts. Then, if you git checkout fix-filterColor-object-type again, the goal will be to make it so that when you run npm run generated-types, and you compare the newly generated file with the old one via diff previousTypesFile.d.ty types/p5.d.ts, you should only see the addition of the FilterColorHook type.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah sure, I'll try comparing the diffs and try to find out what corrections need to be made to get the original structure + filterColorHook type

} else {
type = convertTypeToTypeScript(constant.type, {
isInsideNamespace: false,
isConstantDef: true
});
}
const isMutable = mutableProperties.has(constant.name);
const declaration = isMutable ? 'declare let' : 'declare const';
output += `${declaration} ${constant.name}: ${type};\n\n`;
// Duplicate with a private identifier so we can re-export in the namespace later
output += `${declaration} __${constant.name}: typeof ${constant.name};\n\n`;

});

// Generate main p5 class
Expand All @@ -645,7 +700,6 @@ function generateTypeDefinitions() {
});

// Add strands functions to p5 instance
const strandsMethods = processStrandsFunctions();
strandsMethods.forEach(method => {
output += generateMethodDeclaration(method, p5Options);
});
Expand All @@ -667,9 +721,16 @@ function generateTypeDefinitions() {

output += '\n';


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

if (isTypedefTyped) {
output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: ${constant.type.name};\n`;
} else {
output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`;
}
});

output += '\n';
Expand Down Expand Up @@ -820,4 +881,4 @@ console.log('TypeScript definitions generated successfully!');

// Apply patches
console.log('Applying TypeScript patches...');
applyPatches();
applyPatches();
Loading