diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d28d8b..3145aee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 1.4.0 + +- Sync standalone Custom Code Files: edits to files in `lib/custom_code/` now push to FlutterFlow (previously only custom functions, actions, and widgets synced). + ## 1.3.0 - Support the folder-organized custom code structure: custom functions, actions, and widgets are now synced wherever they live under `lib/` (not just `lib/custom_code/`), discovered from the generated barrel files. diff --git a/package.json b/package.json index 257f423..710393a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "flutterflow-custom-code-editor", "displayName": "FlutterFlow: Custom Code Editor", "description": "Edit your FlutterFlow custom widgets, action, and functions.", - "version": "1.3.0", + "version": "1.4.0", "publisher": "FlutterFlow", "repository": { "type": "git", diff --git a/src/ffState/UpdateManager.ts b/src/ffState/UpdateManager.ts index 1e28f33..9bfd9f2 100644 --- a/src/ffState/UpdateManager.ts +++ b/src/ffState/UpdateManager.ts @@ -319,10 +319,16 @@ export class UpdateManager { } } - // Create new FileInfo with default values - const impliedName = codeType === CodeType.WIDGET ? - toPascalCase(path.basename(filePath, '.dart')) : - toCamelCase(path.basename(filePath, '.dart')); + // Create new FileInfo with default values. A standalone code file's identifier is + // its basename (including .dart), matching the server's FFCustomCodeFile identifier. + let impliedName: string; + if (codeType === CodeType.CODE_FILE) { + impliedName = path.basename(filePath); + } else if (codeType === CodeType.WIDGET) { + impliedName = toPascalCase(path.basename(filePath, '.dart')); + } else { + impliedName = toCamelCase(path.basename(filePath, '.dart')); + } const fileInfo: FileInfo = { old_identifier_name: impliedName, @@ -371,6 +377,7 @@ export class UpdateManager { const isCustomCodeFile = codeType === CodeType.ACTION || codeType === CodeType.WIDGET || + codeType === CodeType.CODE_FILE || (codeType === CodeType.FUNCTION && this._folderOrganized); if (!isCustomCodeFile) { return false; diff --git a/src/fileUtils/FileInfo.ts b/src/fileUtils/FileInfo.ts index 21638b8..aa33d40 100644 --- a/src/fileUtils/FileInfo.ts +++ b/src/fileUtils/FileInfo.ts @@ -14,6 +14,8 @@ export enum CodeType { ACTION = 'A', WIDGET = 'W', FUNCTION = 'F', + // eslint-disable-next-line no-unused-vars + CODE_FILE = 'C', DEPENDENCIES = 'D', OTHER = 'O', } @@ -30,6 +32,9 @@ export function getRelativePath(filePath: string, fileInfo: FileInfo): string { if (fileInfo.type === CodeType.FUNCTION) { return path.posix.join('lib', 'flutter_flow', filePath); } + if (fileInfo.type === CodeType.CODE_FILE) { + return path.posix.join('lib', 'custom_code', filePath); + } if (fileInfo.type === CodeType.DEPENDENCIES) { return filePath; } diff --git a/src/fileUtils/customCodeManifest.ts b/src/fileUtils/customCodeManifest.ts index e015379..800b4f9 100644 --- a/src/fileUtils/customCodeManifest.ts +++ b/src/fileUtils/customCodeManifest.ts @@ -172,6 +172,29 @@ function addBarrelEntries( } } +// Standalone custom code files are generated flat under lib/custom_code/ (never in a +// user-facing subfolder), so the directory is scanned non-recursively. Their identifier +// name is the basename including the .dart extension, matching the server's +// FFCustomCodeFile.identifier.name. +function addCustomCodeFileEntries(manifest: CustomCodeManifest, projectRoot: string) { + const customCodeDir = fullPathFromKey(projectRoot, kCustomCodeDir); + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(customCodeDir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.dart') || entry.name === 'index.dart') { + continue; + } + manifest.set(path.posix.join(kCustomCodeDir, entry.name), { + type: CodeType.CODE_FILE, + identifierName: entry.name, + }); + } +} + // Builds a manifest of all custom code files by parsing the three barrel files. // In folder-organized mode each custom function gets its own entry; in legacy mode // the monolithic custom_functions.dart is the single function entry. @@ -179,6 +202,7 @@ export function buildCustomCodeManifest(projectRoot: string): CustomCodeManifest const manifest: CustomCodeManifest = new Map(); addBarrelEntries(manifest, projectRoot, kActionsBarrelPath, CodeType.ACTION); addBarrelEntries(manifest, projectRoot, kWidgetsBarrelPath, CodeType.WIDGET); + addCustomCodeFileEntries(manifest, projectRoot); const functionsPath = fullPathFromKey(projectRoot, kCustomFunctionsPath); if (!fs.existsSync(functionsPath)) { @@ -236,9 +260,27 @@ export function classifyRelativePath( if (folderOrganized && relativePath.startsWith('lib/custom_code/functions/')) { return CodeType.FUNCTION; } + // A standalone custom code file lives directly under lib/custom_code/ (no further + // subdirectory). Deeper paths are handled by the actions/widgets/functions checks above. + if (isCustomCodeFilePath(relativePath)) { + return CodeType.CODE_FILE; + } return CodeType.OTHER; } +const kCustomCodeDir = 'lib/custom_code/'; + +// True iff the path is exactly lib/custom_code/.dart (flat, not in a subfolder, +// and not the index.dart barrel). Callers must already have ruled out the +// actions/widgets/functions subdirectories. +function isCustomCodeFilePath(relativePath: string): boolean { + if (!relativePath.startsWith(kCustomCodeDir) || !relativePath.endsWith('.dart')) { + return false; + } + const remainder = relativePath.slice(kCustomCodeDir.length); + return !remainder.includes('/') && remainder !== 'index.dart'; +} + /** * Fixes up file maps written by older extension versions: entries migrated from * basename keys may point at the canonical folders while the file actually lives in a diff --git a/src/test/jest/customCodeManifest.test.ts b/src/test/jest/customCodeManifest.test.ts index 02378c1..6f9c545 100644 --- a/src/test/jest/customCodeManifest.test.ts +++ b/src/test/jest/customCodeManifest.test.ts @@ -151,8 +151,10 @@ describe('buildCustomCodeManifest', () => { assert.deepEqual(manifest.get('lib/events/festival/festival_date.dart'), { type: CodeType.FUNCTION, identifierName: 'festivalDate' }); // Falls back to the camelCase basename when no declaration can be parsed assert.deepEqual(manifest.get('lib/custom_code/functions/odd_one.dart'), { type: CodeType.FUNCTION, identifierName: 'oddOne' }); + // Standalone custom code files are tracked by their basename (including .dart) + assert.deepEqual(manifest.get('lib/custom_code/my_helper.dart'), { type: CodeType.CODE_FILE, identifierName: 'my_helper.dart' }); assert.equal(manifest.has('lib/flutter_flow/custom_functions.dart'), false); - assert.equal(manifest.size, 6); + assert.equal(manifest.size, 7); }); it('builds a manifest for a legacy project', () => { @@ -160,7 +162,8 @@ describe('buildCustomCodeManifest', () => { assert.deepEqual(manifest.get('lib/custom_code/actions/my_action.dart'), { type: CodeType.ACTION, identifierName: 'myAction' }); assert.deepEqual(manifest.get('lib/custom_code/widgets/my_widget.dart'), { type: CodeType.WIDGET, identifierName: 'MyWidget' }); assert.deepEqual(manifest.get('lib/flutter_flow/custom_functions.dart'), { type: CodeType.FUNCTION, identifierName: 'CustomFunctions' }); - assert.equal(manifest.size, 3); + assert.deepEqual(manifest.get('lib/custom_code/my_helper.dart'), { type: CodeType.CODE_FILE, identifierName: 'my_helper.dart' }); + assert.equal(manifest.size, 4); }); it('rejects export targets that escape lib/', () => { @@ -213,11 +216,25 @@ describe('classifyRelativePath', () => { // 'transactions' contains 'actions'; the file is still a function assert.equal(classifyRelativePath('lib/custom_code/functions/get_transactions.dart', manifest, true), CodeType.FUNCTION); assert.equal(classifyRelativePath('lib/custom_code/functions/my_widgets_list.dart', manifest, true), CodeType.FUNCTION); - // Files directly under lib/custom_code/ are not in a canonical folder - assert.equal(classifyRelativePath('lib/custom_code/actions_helper.dart', manifest, true), CodeType.OTHER); + // Files directly under lib/custom_code/ are standalone custom code files + assert.equal(classifyRelativePath('lib/custom_code/actions_helper.dart', manifest, true), CodeType.CODE_FILE); + // A file nested in an arbitrary subfolder of lib/custom_code/ is not classifiable assert.equal(classifyRelativePath('lib/custom_code/extractions/foo.dart', manifest, true), CodeType.OTHER); }); + it('classifies standalone custom code files but not the canonical subfolders', () => { + const manifest = buildCustomCodeManifest(folderOrganizedRoot); + assert.equal(classifyRelativePath('lib/custom_code/my_helper.dart', manifest, true), CodeType.CODE_FILE); + assert.equal(classifyRelativePath('lib/custom_code/brand_new.dart', manifest, false), CodeType.CODE_FILE); + // Regression guard: files in the actions/widgets/functions subfolders keep their + // existing classification and must not be captured as standalone code files + assert.equal(classifyRelativePath('lib/custom_code/actions/do_this.dart', manifest, true), CodeType.ACTION); + assert.equal(classifyRelativePath('lib/custom_code/widgets/brand_new.dart', manifest, true), CodeType.WIDGET); + assert.equal(classifyRelativePath('lib/custom_code/functions/brand_new.dart', manifest, true), CodeType.FUNCTION); + // The barrel index file itself is never a standalone code file + assert.equal(classifyRelativePath('lib/custom_code/index.dart', manifest, true), CodeType.OTHER); + }); + it('cannot safely classify new files in arbitrary user folders', () => { const manifest = buildCustomCodeManifest(folderOrganizedRoot); assert.equal(classifyRelativePath('lib/events/festival/brand_new.dart', manifest, true), CodeType.OTHER); diff --git a/src/test/unit/UpdateManager.test.ts b/src/test/unit/UpdateManager.test.ts index 7cbf0d9..7859373 100644 --- a/src/test/unit/UpdateManager.test.ts +++ b/src/test/unit/UpdateManager.test.ts @@ -424,6 +424,27 @@ describe('UpdateManager (folder-organized)', () => { assert.notStrictEqual(result.current_checksum, originalChecksum); assert.strictEqual(updateManager.shouldTrackFile(filePath, 'update'), true); }); + + it('should track edits to a standalone custom code file', async () => { + const filePath = path.join(tempDir, 'lib/custom_code/my_helper.dart'); + assert.strictEqual(updateManager.shouldTrackFile(filePath, 'update'), true); + const originalChecksum = computeChecksum(filePath); + await fs.promises.writeFile(filePath, fs.readFileSync(filePath, 'utf8') + '\n// edited'); + const result = await updateManager.updateFile(filePath); + assert.ok(result); + assert.strictEqual(result.type, 'C'); + assert.strictEqual(result.old_identifier_name, 'my_helper.dart'); + assert.strictEqual(result.new_identifier_name, 'my_helper.dart'); + assert.strictEqual(result.original_checksum, originalChecksum); + assert.notStrictEqual(result.current_checksum, originalChecksum); + }); + + it('should block a tracked custom code file rename', async () => { + const oldPath = path.join(tempDir, 'lib/custom_code/my_helper.dart'); + const newPath = path.join(tempDir, 'lib/custom_code/my_helper_renamed.dart'); + assert.strictEqual(updateManager.blockRename(oldPath, newPath), true); + assert.ok(updateManager.fileMap.has('lib/custom_code/my_helper.dart')); + }); }); // Other test cases (deleteFile, updateFile, etc.) would be converted similarly \ No newline at end of file diff --git a/src/test/util/mockFiles.ts b/src/test/util/mockFiles.ts index 8b4d4e4..54a408e 100644 --- a/src/test/util/mockFiles.ts +++ b/src/test/util/mockFiles.ts @@ -98,6 +98,11 @@ Future createTestRun( `], ['lib/custom_code/actions/index.dart', ` export 'my_action.dart' show myAction; +`], + ['lib/custom_code/my_helper.dart', + `class Helper { + String greet() => 'hi'; +} `], ['pubspec.yaml', ` name: flutter_flow_custom_code_editor @@ -162,6 +167,11 @@ Future planFestival() async { } `], ['lib/custom_code/widgets/index.dart', '// No exports\n'], + ['lib/custom_code/my_helper.dart', + `class Helper { + String greet() => 'hi'; +} +`], ['pubspec.yaml', ` name: flutter_flow_custom_code_editor description: A FlutterFlow custom code editor diff --git a/testdata/folder_organized_project/lib/custom_code/my_helper.dart b/testdata/folder_organized_project/lib/custom_code/my_helper.dart new file mode 100644 index 0000000..656f2b5 --- /dev/null +++ b/testdata/folder_organized_project/lib/custom_code/my_helper.dart @@ -0,0 +1,3 @@ +class Helper { + String greet() => 'hi'; +} diff --git a/testdata/legacy_project/lib/custom_code/my_helper.dart b/testdata/legacy_project/lib/custom_code/my_helper.dart new file mode 100644 index 0000000..656f2b5 --- /dev/null +++ b/testdata/legacy_project/lib/custom_code/my_helper.dart @@ -0,0 +1,3 @@ +class Helper { + String greet() => 'hi'; +}