Skip to content
Draft
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
19 changes: 19 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "tsc: type-check",
"type": "shell",
"command": "npm run type-check",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "never",
"panel": "dedicated"
},
"problemMatcher": "$tsc"
}
]
}
4 changes: 3 additions & 1 deletion locales/en/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,7 @@

"prompt.update.deleteLib.message": "Delete the local lib/ directory?",

"prompt.cancel.sketchCreation": "You cancelled the sketch creation"
"prompt.cancel.sketchCreation": "You cancelled the sketch creation",

"prompt.cancel.sketchUpdate": "You cancelled the sketch update"
}
49 changes: 49 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"scripts": {
"test": "vitest run",
"test:coverage": "vitest --coverage --run",
"run": "node ./index.js"
"run": "node ./index.js",
"type-check": "tsc"
},
"dependencies": {
"@clack/prompts": "^0.11.0",
Expand All @@ -40,7 +41,11 @@
"unique-names-generator": "^4.7.1"
},
"devDependencies": {
"@types/degit": "^2.8.6",
"@types/minimist": "^1.2.5",
"@types/node": "^25.0.2",
"@vitest/coverage-v8": "^1.0.0",
"typescript": "^5.9.3",
"vitest": "^1.0.0"
}
}
74 changes: 66 additions & 8 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,84 @@
import fs from 'fs/promises';
import path from 'path';
import { readJSON, writeJSON, fileExists } from './utils.js';
import { messageFromErrorOrUndefined } from './exceptionUtils.js';

/**
* @typedef {import('./types.js').Language} Language
*/
/**
* @typedef {import('./types.js').P5Mode} P5Mode
*/
/**
* @typedef {import('./types.js').DeliveryMode} DeliveryMode
*/
/**
* @typedef {import('./types.js').SetupType} SetupType
*/



/**
* @param {any} candidate
* @returns {candidate is DeliveryMode}
*/
export function isValidDeliveryMode(candidate){
return (candidate==="cdn" || candidate==="local");
}

/**
* @param {any} candidate
* @returns {candidate is P5Mode}
*/
export function isValidP5Mode(candidate){
return (candidate==="global" || candidate==="instance");
}

/**
* @param {any} candidate
* @returns {candidate is Language}
*/
export function isValidLanguage(candidate){
return (candidate==="javascript" || candidate==="typescript");
}

/**
* @typedef {Object} ProjectConfig
* @property {string} version
* @property {DeliveryMode} mode
* @property {Language?} language
* @property {P5Mode?} p5Mode
* @property {string|null} typeDefsVersion
* @property {string=} template
* @property {string} lastUpdated

*
*/
/**
* Creates a new .p5-config.json file with project metadata
*
* @param {string} configPath - The path where the config file should be created
* @param {Object} options - Configuration options
* @param {string} options.version - The p5.js version used
* @param {string} [options.mode='cdn'] - Delivery mode: "cdn" or "local"
* @param {string} [options.language] - Programming language: "javascript" or "typescript"
* @param {string} [options.p5Mode] - p5.js mode: "global" or "instance"
* @param {DeliveryMode} [options.mode='cdn'] - Delivery mode: "cdn" or "local"
* @param {Language} [options.language] - Programming language: "javascript" or "typescript"
* @param {P5Mode} [options.p5Mode] - p5.js mode: "global" or "instance"
* @param {string} [options.template] - template
* @param {string|null} [options.typeDefsVersion=null] - Version of TypeScript definitions installed
* @returns {Promise<void>}
*/
export async function createConfig(configPath, options) {
/**
* @type {ProjectConfig}
*/
const config = {
version: options.version,
mode: options.mode || 'cdn',
language: options.language || null,
p5Mode: options.p5Mode || null,
typeDefsVersion: options.typeDefsVersion || null,
lastUpdated: new Date().toISOString()
typeDefsVersion: options.typeDefsVersion || null,
lastUpdated: new Date().toISOString(),
template: options.template
};

await writeJSON(configPath, config);
Expand All @@ -35,7 +92,7 @@ export async function createConfig(configPath, options) {
* Reads an existing .p5-config.json file
*
* @param {string} configPath - The path to the config file
* @returns {Promise<Object|null>} The configuration object with {version, mode, language, p5Mode, typeDefsVersion, lastUpdated} or null if file doesn't exist
* @returns {Promise<ProjectConfig|null>} The configuration object with {version, mode, language, p5Mode, typeDefsVersion, lastUpdated} or null if file doesn't exist
*/
export async function readConfig(configPath) {
return await readJSON(configPath);
Expand Down Expand Up @@ -80,9 +137,10 @@ export async function migrateConfigIfNeeded(projectDir) {
await fs.rename(oldConfigPath, newConfigPath);
return { migrated: true, error: null };
} catch (err) {

return {
migrated: false,
error: `error.migration.renameFailed: ${err.message}`
migrated: false,
error: `error.migration.renameFailed: ${messageFromErrorOrUndefined(err)}`
};
}
}
78 changes: 78 additions & 0 deletions src/exceptionUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

/**
* Type guard to check if an unknown value is an Error object.
* @param {unknown} error
* @returns {error is Error}
*/
export function isError(error) {
// Check if it's an instance of Error and also check for common object-ness
// in case the error came from a different environment/iframe.
return error instanceof Error && typeof error.message === 'string';
}

/**
* @typedef {object} FetchErrorCandidate
* @property {string} message - The error message string.
* @property {string} [code] - An optional error code string (e.g., 'ENOTFOUND').
*/

/**
* Type guard to check if an unknown value is an object that is likely a Node.js-style error
* with at least a 'message' property. Allows us to safely check if the object has a .code property later without checking again if it IS an object.
* * @param {unknown} error - The value caught by the catch block.
* @returns {error is FetchErrorCandidate} True if the error has the expected structure.
*/
export function isFetchErrorCandidate(error) {
if (typeof error !== 'object' || error === null) {
return false;
}
/** @type {any} */
const e = error;

// Check if 'message' property exists and is a string.
return typeof e.message === 'string';
}


/**
* Type guard to check if an unknown value is an object with a string "message" property.
* * @param {unknown} error - an error (likely from a catch block)
* @returns {error is Record<"message", string>} True if the error has the expected structure.
*/
export function hasMessageStringProperty(error) {
/**@type {any} */
const e = error;
return typeof e.message === "string";
}

/**
* @typedef {object} LoggingError
* @property {string} message
* @property {string | undefined} stack - The stack trace, which may be present but is sometimes undefined.
*/
/**
* Type guard to check if an unknown value is an object with a message and stack property.
* @param {unknown} error - The value caught by the catch block.
* @returns {error is LoggingError} True if the error has the required structure.
*/
export function isLoggingError(error) {
// 1. Basic checks for object type and null
if (typeof error !== 'object' || error === null) {
return false;
}
/** @type {any} */
const e = error;
const hasMessage = typeof e.message === 'string';
const hasStackProperty = (typeof e.stack === 'string' || typeof e.stack === 'undefined');
return hasMessage && hasStackProperty;
}

/**
* @param {unknown} error - likely from a "catch"
*/
export function messageFromErrorOrUndefined(error) {
if (hasMessageStringProperty(error)){
return error.message;
}
return undefined;
}
4 changes: 2 additions & 2 deletions src/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function initGit(projectDir) {
}

// Run git init
await new Promise((resolve, reject) => {
await /** @type {Promise<void>} */(new Promise((resolve, reject) => {
const git = spawn('git', ['init'], { cwd: projectDir });

git.on('error', (error) => {
Expand All @@ -48,7 +48,7 @@ export async function initGit(projectDir) {
reject(new Error(`git init exited with code ${code}`));
}
});
});
}));

// Create .gitignore file
await createGitignore(projectDir);
Expand Down
Loading