Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 11 additions & 4 deletions src/ffState/UpdateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/fileUtils/FileInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Expand All @@ -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;
}
Expand Down
42 changes: 42 additions & 0 deletions src/fileUtils/customCodeManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,37 @@ 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.
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)) {
Expand Down Expand Up @@ -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/<name>.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
Expand Down
25 changes: 21 additions & 4 deletions src/test/jest/customCodeManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,19 @@ 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', () => {
const manifest = buildCustomCodeManifest(legacyRoot);
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/', () => {
Expand Down Expand Up @@ -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);
Expand Down
21 changes: 21 additions & 0 deletions src/test/unit/UpdateManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions src/test/util/mockFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ Future<DocumentReference?> 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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Helper {
String greet() => 'hi';
}
3 changes: 3 additions & 0 deletions testdata/legacy_project/lib/custom_code/my_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Helper {
String greet() => 'hi';
}