From 3e3872b4b2c5cdd8938eadd3235919abcbc97dd3 Mon Sep 17 00:00:00 2001 From: Souvik Biswas Date: Thu, 18 Jun 2026 14:31:41 +0530 Subject: [PATCH 1/2] Sync standalone Custom Code Files (ENG-12383) The extension only tracked custom functions/actions/widgets via the three barrel files, so standalone Custom Code Files (generated flat under lib/custom_code/.dart) classified as OTHER and were never pushed. Add a CODE_FILE ('C') type that classifies any *.dart directly under lib/custom_code/ (excluding the actions/widgets/functions subfolders and index.dart) and scan that directory non-recursively when building the manifest. A code file's identifier is its basename including .dart, matching the server's FFCustomCodeFile.identifier.name. Renames of code files are blocked like other custom code, and they are tracked (no "won't sync" warning) and pushed by basename via the existing wire file map. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LmtfBu7w6GTyDUa2j2gKWR --- src/ffState/UpdateManager.ts | 15 +++++-- src/fileUtils/FileInfo.ts | 5 +++ src/fileUtils/customCodeManifest.ts | 42 +++++++++++++++++++ src/test/jest/customCodeManifest.test.ts | 25 +++++++++-- src/test/unit/UpdateManager.test.ts | 21 ++++++++++ src/test/util/mockFiles.ts | 10 +++++ .../lib/custom_code/my_helper.dart | 3 ++ .../lib/custom_code/my_helper.dart | 3 ++ 8 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 testdata/folder_organized_project/lib/custom_code/my_helper.dart create mode 100644 testdata/legacy_project/lib/custom_code/my_helper.dart 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'; +} From 48f30a65c4a6d7c1c2de8ca68d53f5b27e929b8b Mon Sep 17 00:00:00 2001 From: Souvik Biswas Date: Thu, 18 Jun 2026 14:39:22 +0530 Subject: [PATCH 2/2] Release 1.4.0: sync standalone Custom Code Files --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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",