Guides · Version control
Import VBA Modules and UserForms Without Breaking References
Published · Tested with Microsoft 365 Excel 16.0.20430.20092 (64-bit), PowerShell 7.6.6, Windows 11 Pro
Exporting a workbook's VBA to text is the easy half. Putting it back into another workbook is where things break: Import renames a module that already exists, a UserForm without its .frx refuses to load, the code of ThisWorkbook arrives as a new class, and code that ran yesterday stops with “Can't find project or library”. This guide reproduces each of those in Excel and builds one PowerShell script that imports into a copy of the workbook, restores the references and checks the result by exporting it again.
Before you start
- An exported folder. This guide starts where Excel VBA Version Control with Git: Export and Verify ends: a folder with one
.bas,.clsor.frm+.frxfile per component, made by itsexport-vba.ps1. Step 1 below adds a few lines to that script. - The same Trust Center setting. Reading or changing a VBA project from code needs Trust access to the VBA project object model; see One setting to know about for why it is a security decision. Neither script changes it.
- Windows with Excel desktop and a person signed in. The import runs in its own hidden Excel and closes only that one, with the pattern from Close Only the Excel Your PowerShell Script Started.
Every output below was copied from test scripts written for this guide and run three times each. The source workbook, orders.xlsm, is invented: a standard module Orders, a class OrderLine, code in ThisWorkbook and Sheet1, a UserForm frmOrder with a label, a text box, a button and an image, and a reference to Microsoft Scripting Runtime for a Scripting.Dictionary.
What goes wrong when you import
VBComponents.Import “adds a component to a project from a file; returns the newly added component” (Microsoft Learn). The page says nothing about what happens when the name is taken or the file is not a plain module. Into a new, empty workbook:
1. A form without its .frx. The .frm holds the form's code and a pointer, OleObjectBlob = "frmOrder.frx":0000; the controls themselves are in the binary .frx. Leave it behind (an email attachment filter, a .gitignore, a copy of “just the code”) and the import fails:
Import failed: Errors during load. Refer to '.\frmOrder.log' for details (0x800AEA9D)
components after: ThisWorkbook(100), Sheet1(100)
# frmOrder.log, written next to the .frm
Line 8: Property OleObjectBlob in frmOrder had an invalid file reference.
No half-loaded form stays behind; you only get an error and a log file. The .frx is not optional decoration: the same form exported to a 2,584-byte .frx without its image control and 9,752 bytes with it.
2. A name that already exists. The target workbook already had an older Orders module, an OrderLine class and a frmOrder form (numbers are component types: 1 module, 2 class, 3 form, 100 sheet or workbook):
before: ThisWorkbook(100), Sheet1(100), Orders(1), frmOrder(3), OrderLine(2)
Import over existing: Orders.bas -> Orders1, OrderLine.cls -> OrderLine1
frmOrder.frm over existing: Errors during load. Refer to '.\frmOrder.log' for details
after: ThisWorkbook(100), Sheet1(100), Orders(1), frmOrder(3), OrderLine(2), Orders1(1), OrderLine1(2)
# frmOrder.log
Line 2: The Form or MDIForm name frmOrder is already in use; cannot load this form.
The module and the class came in next to the old ones, as Orders1 and OrderLine1, with no error; any code that calls Orders.CountCustomers still runs the old version. The form failed outright. Removing the old component by its exact name first, then importing, gave the right names in all three runs:
Remove then Import: Orders.bas -> Orders, OrderLine.cls -> OrderLine, frmOrder.frm -> frmOrder (4 controls)
3. ThisWorkbook and sheet code. Their files are exported as .cls, and importing them does what it does for any class:
ThisWorkbook.cls -> ThisWorkbook1 (type 2); Sheet1.cls -> Sheet11 (type 2)
ThisWorkbook module lines: 0
Two new class modules, and the real ThisWorkbook still empty: event handlers such as Workbook_Open or Worksheet_Change would never fire from a class. Nothing in the exported header names the sheet either; the only sign is a pair of attributes an ordinary class does not have:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "ThisWorkbook"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Option Explicit
The fix is to leave the document module where it is and replace its code: CodeModule.DeleteLines, then CodeModule.AddFromString with the file minus that header.
4. Every import adds a blank line to a form. We imported frmOrder.frm into a new workbook, exported it, imported that export, three times in a row. The file grew by one line each time (20, 21, 22, 23 lines), all of them empty lines between the header and Option Explicit. Harmless to run, but every round trip shows up as a change in git, and the import check below would fail on it. The script removes the added lines after each form import.
5. The .frx changes on every export. Four exports of the same, unchanged orders.xlsm:
frmOrder.frm : 75A1DA7C 75A1DA7C 75A1DA7C 75A1DA7C
frmOrder.frx : C3A7139A E6DFEEF1 FE0DE9ED D9090197
Orders.bas : 8CDB3F9B 8CDB3F9B 8CDB3F9B 8CDB3F9B
ThisWorkbook.cls : BD73C173 BD73C173 BD73C173 BD73C173
(The first 8 characters of each SHA256.) Eight bytes at offset 1156 of this .frx are a Windows timestamp equal to the time of the export, and between 5 and 12 other bytes changed from one export to the next. So a byte-for-byte comparison of a .frx never passes, and in git the .frx changes whenever you export, even if nobody touched the form.
Why the references break
References (Tools › References in the VBA editor) belong to the project, not to the modules, so no exported file carries them. Import Orders.bas into a workbook that lacks Microsoft Scripting Runtime and run CountCustomers, which declares Dim seen As Scripting.Dictionary. The VBA editor opens with this message (read from the dialog by our test script, three runs out of three):
Compile error:
User-defined type not defined
The other classic is a reference that exists but points nowhere. We made the target reference a small library workbook, pricing-lib.xlsm, saved it, and moved the library away. The reference list then shows it as broken, with the path as its name (paths shortened):
Scripting {420B2830-E718-11CF-893D-00A0C9054228} 1.0 broken=False path=C:\Windows\System32\scrrun.dll
...\lib\pricing-lib.xlsm 0.0 broken=True path=...\lib\pricing-lib.xlsm
Then we ran Stamp, a function that uses only VBA's own Left$, Format$ and Date, nothing from the library:
Compile error:
Can't find project or library
That is the confusing part: the error points at a built-in function, not at the reference. Microsoft's page on this error is blunt about it: “You can't run your code until all missing references are resolved”, and “unresolved references are prefixed with MISSING in the References dialog box” (Microsoft Learn). From code, the same state is Reference.IsBroken, which tells “whether the Reference object points to a valid reference in the registry” (Microsoft Learn).
Adding a reference back is References.AddFromGuid(guid, major, minor), which “searches the registry to find the reference that you want to add” (Microsoft Learn). What it did on this PC, three runs each:
AddFromGuid with a GUID not on this PC : Object library not registered (0x8002801D)
AddFromGuid Scripting, version 9.0 : Object library not registered (0x8002801D)
AddFromGuid Scripting, version 0.0 : added, as version 1.0
AddFromGuid Scripting, already there : Name conflicts with existing module, project, or object library (0x800A802D)
So the import needs the GUID and version of every reference the source had, and has to skip the ones already there. One more detail from the export: adding a UserForm to a workbook adds a reference to Microsoft Forms 2.0 on its own, and the list should carry it too.
Step 1: export the references too
Add these lines to export-vba.ps1 from the git guide, inside the try block, right after the foreach loop that exports the components:
# References the code needs, for the import step (VBA and Excel itself are built in).
$references = @(foreach ($ref in $book.VBProject.References) {
if ($ref.BuiltIn) { continue }
[ordered]@{ name = $ref.Name; guid = $ref.GUID; major = $ref.Major; minor = $ref.Minor
fullPath = $(try { $ref.FullPath } catch { '' }); isBroken = $ref.IsBroken }
})
ConvertTo-Json -InputObject $references -Depth 3 |
Set-Content -LiteralPath (Join-Path $staging 'vba-references.json') -Encoding utf8
The file is written before the manifest, so the manifest lists it and the manifest check covers it. VBA and Excel's own library are left out: every workbook has them and they cannot be removed. For orders.xlsm:
[
{
"name": "stdole",
"guid": "{00020430-0000-0000-C000-000000000046}",
"major": 2,
"minor": 0,
"fullPath": "C:\\Windows\\System32\\stdole2.tlb",
"isBroken": false
},
{
"name": "Office",
"guid": "{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}",
"major": 2,
"minor": 8,
"fullPath": "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE16\\MSO.DLL",
"isBroken": false
},
{
"name": "Scripting",
"guid": "{420B2830-E718-11CF-893D-00A0C9054228}",
"major": 1,
"minor": 0,
"fullPath": "C:\\Windows\\System32\\scrrun.dll",
"isBroken": false
},
{
"name": "MSForms",
"guid": "{0D452EE1-E08F-101A-852E-02608C4D0BB4}",
"major": 2,
"minor": 0,
"fullPath": "C:\\WINDOWS\\system32\\FM20.DLL",
"isBroken": false
}
]
Step 2: import into a copy
import-vba.ps1 never touches the workbook you give it. It copies it, imports into the copy, and keeps the copy only if the check at the end passes:
# import-vba.ps1: imports a folder made by export-vba.ps1 into a COPY of a workbook, restores
# its references, and checks the result by exporting it again. Needs "Trust access to the VBA
# project object model" (File > Options > Trust Center), which this script never changes.
# Exit codes: 0 imported and checked; 1 the check failed (the copy is deleted);
# 2 bad input (nothing written); 3 the VBA project cannot be read (copy deleted).
param(
[Parameter(Mandatory)] [string] $Workbook, # imported into a copy; never changed
[Parameter(Mandatory)] [string] $VbaDir, # .bas/.cls/.frm/.frx + vba-references.json
[Parameter(Mandatory)] [string] $OutputWorkbook # the copy to create; must not exist
)
$ErrorActionPreference = 'Stop'
$extensions = @{ 1 = '.bas'; 2 = '.cls'; 3 = '.frm'; 100 = '.cls' } # module, class, form, sheet/workbook
Add-Type -Namespace Win32Tools -Name Native -MemberDefinition @'
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint processId);
[DllImport("kernel32.dll")]
public static extern uint GetACP();
'@
# The VBA editor reads and writes these files in the Windows ANSI code page, not UTF-8.
$ansi = [Text.Encoding]::GetEncoding([int] [Win32Tools.Native]::GetACP())
function Get-ReferencePath($Reference) {
# A MISSING type library can throw on FullPath: an empty path then, not a failed run.
try { $Reference.FullPath } catch { '' }
}
function Get-ComponentName([string[]] $Lines) {
foreach ($line in $Lines) { if ($line -match '^Attribute VB_Name = "(.+)"$') { return $Matches[1] } }
}
function Get-CodeBody([string[]] $Lines) {
# Skips the header the editor writes (VERSION, BEGIN...END, Attribute VB_*): only the code
# goes into a document module.
$i = 0
if ($Lines.Count -gt 0 -and $Lines[0] -like 'VERSION *') { $i++ }
if ($i -lt $Lines.Count -and $Lines[$i] -cmatch '^(BEGIN|Begin)\b') {
while ($i -lt $Lines.Count -and $Lines[$i] -cnotmatch '^(END|End)$') { $i++ }
$i++
}
while ($i -lt $Lines.Count -and $Lines[$i] -like 'Attribute VB_*') { $i++ }
if ($i -ge $Lines.Count) { return '' }
$Lines[$i..($Lines.Count - 1)] -join "`r`n"
}
function Remove-AddedBlankLines($CodeModule, [string] $Body) {
# Each import of a form adds an empty line at the top of its code: keep only the original ones.
$wanted = 0
while ($Body.Substring([Math]::Min($wanted * 2, $Body.Length)).StartsWith("`r`n")) { $wanted++ }
$blank = 0
while ($blank -lt $CodeModule.CountOfLines -and $CodeModule.Lines($blank + 1, 1) -eq '') { $blank++ }
if ($blank -gt $wanted) { $CodeModule.DeleteLines(1, $blank - $wanted) }
}
function Compare-Export([string] $Name, [switch] $SizeOnly) {
$expected = Join-Path $dir $Name
$actual = Join-Path $checkDir $Name
if (-not (Test-Path -LiteralPath $actual)) { $problems.Add("${Name}: not exported again"); return }
$same = if ($SizeOnly) { (Get-Item -LiteralPath $expected).Length -eq (Get-Item -LiteralPath $actual).Length }
else { (Get-FileHash -LiteralPath $expected).Hash -eq (Get-FileHash -LiteralPath $actual).Hash }
if (-not $same) { $problems.Add("${Name}: differs from the folder after the import") }
}
function Test-DocumentHeader([string[]] $Lines) {
# ThisWorkbook and sheet modules export as classes with both of these set.
($Lines -contains 'Attribute VB_PredeclaredId = True') -and ($Lines -contains 'Attribute VB_Exposed = True')
}
# --- Checks before anything is written (exit 2) ---
$source = Resolve-Path -LiteralPath $Workbook -ErrorAction SilentlyContinue
$dir = Resolve-Path -LiteralPath $VbaDir -ErrorAction SilentlyContinue
$output = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputWorkbook)
$inputErrors = [Collections.Generic.List[string]]::new()
if (-not $source) { $inputErrors.Add("workbook not found: $Workbook") }
if (-not $dir) { $inputErrors.Add("folder not found: $VbaDir") }
if (Test-Path -LiteralPath $output) { $inputErrors.Add("output already exists: $OutputWorkbook") }
if ($source -and [IO.Path]::GetExtension($source) -notin '.xlsm', '.xlsb', '.xls') {
$inputErrors.Add("$Workbook cannot keep macros: use a .xlsm, .xlsb or .xls workbook")
}
if ($source -and [IO.Path]::GetExtension($output) -ne [IO.Path]::GetExtension($source)) {
$inputErrors.Add("the copy must keep the extension of $Workbook")
}
if (-not (Test-Path -LiteralPath (Split-Path -Parent $output) -PathType Container)) {
$inputErrors.Add("folder of the output not found: $OutputWorkbook")
}
if ($dir) {
$files = @(Get-ChildItem -LiteralPath $dir -File | Where-Object Extension -in '.bas', '.cls', '.frm' | Sort-Object Name)
if ($files.Count -eq 0) { $inputErrors.Add("no .bas, .cls or .frm files in $VbaDir") }
foreach ($form in $files | Where-Object Extension -eq '.frm') {
$frx = [IO.Path]::ChangeExtension($form.FullName, '.frx')
if (-not (Test-Path -LiteralPath $frx)) { $inputErrors.Add("$($form.Name): $(Split-Path -Leaf $frx) is missing next to it") }
}
$referencesFile = Join-Path $dir 'vba-references.json'
if (-not (Test-Path -LiteralPath $referencesFile)) { $inputErrors.Add('vba-references.json is missing: export again') }
else {
try { $wantedReferences = @(Get-Content -LiteralPath $referencesFile -Raw | ConvertFrom-Json) }
catch { $inputErrors.Add("vba-references.json is not valid JSON: $($_.Exception.Message)") }
}
}
if ($inputErrors.Count -gt 0) {
Write-Output "ERROR: nothing imported"; $inputErrors | ForEach-Object { Write-Output " - $_" }; exit 2
}
# --- Import into the copy ---
Copy-Item -LiteralPath $source -Destination $output
$excel = $excelProcess = $null
$checkDir = "$output.check-" + [guid]::NewGuid().ToString('N')
$problems = [Collections.Generic.List[string]]::new()
$notes = [Collections.Generic.List[string]]::new()
$checked = [Collections.Generic.List[string]]::new() # files imported, to compare after re-export
$exitCode = 0
try {
$excel = New-Object -ComObject Excel.Application
$excelPid = [uint32] 0
[void] [Win32Tools.Native]::GetWindowThreadProcessId([IntPtr] $excel.Hwnd, [ref] $excelPid)
$excelProcess = Get-Process -Id $excelPid
$excel.DisplayAlerts = $false
$excel.AutomationSecurity = 3 # msoAutomationSecurityForceDisable: no macro runs
$book = $excel.Workbooks.Open($output, 0)
try { $project = $book.VBProject; [void] $project.VBComponents.Count }
catch { $exitCode = 3; throw 'Cannot read the VBA project: "Trust access to the VBA project object model" is off.' }
# References first, so the imported code compiles against them.
foreach ($ref in $wantedReferences) {
$present = $false
foreach ($have in $project.References) {
if (($ref.guid -and $have.GUID -eq $ref.guid) -or ($ref.fullPath -and (Get-ReferencePath $have) -eq $ref.fullPath)) { $present = $true }
}
if ($present) { continue }
try {
if ($ref.guid) { [void] $project.References.AddFromGuid($ref.guid, $ref.major, $ref.minor) }
else { [void] $project.References.AddFromFile($ref.fullPath) } # a reference to another workbook
$notes.Add("reference added: $($ref.name) $($ref.major).$($ref.minor)")
}
catch { $problems.Add("reference $($ref.name) $($ref.major).$($ref.minor) not added: $($_.Exception.Message)") }
}
foreach ($ref in $project.References) {
if ($ref.IsBroken) { $problems.Add("reference broken (MISSING): $($ref.Name) $(Get-ReferencePath $ref)") }
}
foreach ($file in $files) {
$lines = [IO.File]::ReadAllLines($file.FullName, $ansi)
$name = Get-ComponentName $lines
if ($name -ne $file.BaseName) { $problems.Add("$($file.Name): its Attribute VB_Name is '$name', not '$($file.BaseName)'"); continue }
$existing = $null
foreach ($component in $project.VBComponents) { if ($component.Name -eq $name) { $existing = $component } }
if ($existing -and $existing.Type -eq 100) {
# ThisWorkbook or a sheet: Import would add a class named ThisWorkbook1 instead.
$code = $existing.CodeModule
if ($code.CountOfLines -gt 0) { $code.DeleteLines(1, $code.CountOfLines) }
$body = Get-CodeBody $lines
if ($body) { $code.AddFromString($body) }
$checked.Add($file.Name)
continue
}
if ($file.Extension -eq '.cls' -and (Test-DocumentHeader $lines)) {
$problems.Add("$($file.Name): code of a sheet or ThisWorkbook, but this workbook has no module named $name"); continue
}
# Same name: Import would add Orders as Orders1 (and fail for a form). Remove it first.
if ($existing) { $project.VBComponents.Remove($existing) }
$imported = $project.VBComponents.Import($file.FullName)
if ($imported.Name -ne $name) { $problems.Add("$($file.Name): imported as $($imported.Name), not $name"); continue }
if ($file.Extension -eq '.frm') { Remove-AddedBlankLines $imported.CodeModule (Get-CodeBody $lines) }
$checked.Add($file.Name)
}
# Check: export the copy again and compare with the folder it came from. Code files must be
# identical; a .frx only the same size (Excel writes the export time and other bytes into it).
New-Item -ItemType Directory -Path $checkDir | Out-Null
foreach ($component in $project.VBComponents) {
$fileName = $component.Name + $extensions[[int] $component.Type]
if ($fileName -in $checked) { $component.Export((Join-Path $checkDir $fileName)); continue }
$hasCode = $component.Type -ne 100 -or $component.CodeModule.CountOfLines -gt 0
if ($hasCode -and $fileName -notin $files.Name) { $notes.Add("kept, not in the folder: $fileName") }
}
foreach ($name in $checked) {
Compare-Export $name
if ($name -like '*.frm') { Compare-Export ([IO.Path]::ChangeExtension($name, '.frx')) -SizeOnly }
}
if ($problems.Count -eq 0) { $book.Save() }
$book.Close($false)
}
catch {
if ($exitCode -eq 0) { $exitCode = 1 }
$problems.Add($_.Exception.Message)
}
finally {
# Close only this Excel: release COM objects, Quit, wait, and end this process if it hangs.
$book = $project = $code = $existing = $imported = $component = $ref = $have = $null
$Error.Clear() # error records can hold COM objects too
[GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect(); [GC]::WaitForPendingFinalizers()
if ($excel) {
try { $excel.Quit() } catch { Write-Warning "Quit: $($_.Exception.Message)" }
[void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($excel)
}
if ($excelProcess) {
$deadline = [DateTime]::UtcNow.AddSeconds(10)
while (-not $excelProcess.HasExited -and [DateTime]::UtcNow -lt $deadline) { Start-Sleep -Milliseconds 250 }
if (-not $excelProcess.HasExited) {
Write-Warning "Excel $($excelProcess.Id) still running 10 s after Quit(); ending it."
try { $excelProcess.Kill(); [void] $excelProcess.WaitForExit(5000) } catch { } # it may exit on its own meanwhile
}
}
Remove-Item -LiteralPath $checkDir -Recurse -Force -ErrorAction SilentlyContinue
}
$notes | ForEach-Object { Write-Output " $_" }
if ($problems.Count -gt 0) {
Remove-Item -LiteralPath $output -Force -ErrorAction SilentlyContinue # never leave a half-imported copy
$kept = if (Test-Path -LiteralPath $output) { 'could NOT be deleted: do not use it' } else { 'not kept' }
Write-Output "FAIL: $($problems.Count) problem(s); $OutputWorkbook $kept"
$problems | ForEach-Object { Write-Output " - $_" }
if ($exitCode -eq 0) { $exitCode = 1 }
exit $exitCode
}
Write-Output "OK: $($files.Count) file(s) imported into $OutputWorkbook; the re-export matches the folder"
exit 0
What each part answers, from the tests above:
- Checks before writing anything. A
.frmwithout its.frx, a missing or unreadable references file, a workbook that cannot hold macros (.xlsx), a missing output folder or an existing output stop the run with exit code 2 and no copy, instead of the “Errors during load” log. - References first. Each one is added by GUID and its exact version, or skipped if the copy already has it; then any reference that is still broken fails the run. A reference to another workbook has no GUID, so it is added from its path.
- Document modules by code, everything else by import. A file whose name matches a sheet or
ThisWorkbookin the copy replaces that module's code. A.clsthat looks like document code with no matching module is a problem, not a new class. Any other component with the same name is removed first, soOrdersstaysOrders. A file whose name does not match itsAttribute VB_Nameis a problem too, because the check could not find it again. - The blank lines a form import adds are removed. Only the extra ones: if the original code starts with an empty line, it stays.
- The check is a second export. Every imported component is exported again and compared with the file it came from:
.bas,.clsand.frmby SHA256, the.frxby size only, because Excel writes the time into it. - Files are read in the Windows ANSI code page, the one the editor writes them in, not as UTF-8.
- Exit codes. 0 imported and checked; 1 the import or the check failed; 2 bad input, nothing written; 3 the VBA project could not be read. On 1 and 3 the copy is deleted: a half-imported workbook is not something to deliver by mistake.
Results
The target, target.xlsm, has an older Orders module, a module of its own (Helpers) and no Scripting Runtime reference. Output of the third run; the first two printed the same lines, apart from the process ids and which runs needed the fallback (see Limits):
PS> ./import-vba.ps1 -Workbook target.xlsm -VbaDir vba -OutputWorkbook target-v2.xlsm
WARNING: Excel 40752 still running 10 s after Quit(); ending it.
reference added: Scripting 1.0
reference added: MSForms 2.0
kept, not in the folder: Helpers.bas
OK: 5 file(s) imported into target-v2.xlsm; the re-export matches the folder
(exit code 0)
PS> ./import-vba.ps1 -Workbook target-v2.xlsm -VbaDir vba -OutputWorkbook target-v2b.xlsm
kept, not in the folder: Helpers.bas
OK: 5 file(s) imported into target-v2b.xlsm; the re-export matches the folder
(exit code 0)
The second import, into a copy of the first copy, is the round trip that used to add blank lines to the form; it passes the same check. Opened with macros enabled, the second copy runs the new code, keeps its own module, and has its form and references back:
Orders.CountCustomers = 2
Helpers.Twice(21) = 42
ThisWorkbook code: Option Explicit | | Public Function BuiltFor() As String | BuiltFor = "orders demo" | End Function
frmOrder controls: lblSku, txtSku, cmdOK, imgLogo; imgLogo.Picture.Width (from VBA): 847
frmOrder first code line: [Option Explicit]
references: VBA, Excel, stdole, Office, Scripting, MSForms
And the failures, each ending without a copy (the original target.xlsm had the same SHA256 after all runs):
# the .frx deleted from the folder
ERROR: nothing imported
- frmOrder.frm: frmOrder.frx is missing next to it
(exit code 2)
# a reference added to vba-references.json with a GUID that is not on this PC
WARNING: Excel 46756 still running 10 s after Quit(); ending it.
reference added: Scripting 1.0
reference added: MSForms 2.0
kept, not in the folder: Helpers.bas
FAIL: 1 problem(s); target-v4.xlsm not kept
- reference LabelPrinter 1.0 not added: Object library not registered
(exit code 1)
# a macro-free copy of the target
ERROR: nothing imported
- target-nomacros.xlsx cannot keep macros: use a .xlsm, .xlsb or .xls workbook
(exit code 2)
# a target whose first sheet has the code name Data instead of Sheet1
WARNING: Excel 32744 still running 10 s after Quit(); ending it.
reference added: Scripting 1.0
reference added: MSForms 2.0
kept, not in the folder: Helpers.bas
FAIL: 1 problem(s); target-v5.xlsm not kept
- Sheet1.cls: code of a sheet or ThisWorkbook, but this workbook has no module named Sheet1
(exit code 1)
Limits
- It does not compile the project. We tried running the editor's Debug › Compile VBAProject command from code (
VBE.CommandBars.FindControl(Id:=578).Execute()). On a project that compiled, the command became disabled (3 runs of 3); on one with a missing reference it stayed enabled and the “Compile error” dialog opened and blocked Excel (3 of 3). That control ID is not a documented API and the dialog needs a click, so the script does not use it. Open the copy once and compile it by hand before you deliver it. - A
.frxis checked by size, not content. A form whose controls changed but whose.frxkept its size would pass. The.frm, with the form's code and its own properties, is compared exactly. - Document modules are matched by code name. We tested English code names (
ThisWorkbook,Sheet1); a workbook whose sheet has another code name fails the run with a message rather than guessing. A class module whoseVB_PredeclaredIdattribute was set toTrueby hand would be taken for document code. - Sheet code that the source emptied is kept. The export skips sheet and
ThisWorkbookmodules with no code, so the folder cannot say “this module is now empty”. If the target still has code there (an oldWorkbook_Open, say), the script keeps it and printskept, not in the folder: ThisWorkbook.cls. Read those lines before you deliver. - Only the header is stripped from document code. Procedure attributes inside a sheet or
ThisWorkbookfile (for exampleAttribute BuildReport.VB_Description) would be added as text; we did not test that case. - References must exist on the PC that imports. The script restores them; it cannot install a library. A reference to another workbook is restored from its path, which we read but did not test restoring.
- We did not test it with the Trust Center setting off. We did not change that setting on the test PC. The script reads the project inside a
try, as the export script does, and exits with code 3 if that fails. - The fallback ends Excel in most runs. In 9 of 12 runs of the final script, the hidden Excel was still running 10 seconds after
Quit()and the script ended that process, so a run took about 12 seconds instead of 4. A second garbage collection andforeachloops instead of pipelines over the COM collections did not reduce it (6 of 12 runs before those changes). The result was the same either way, and the workbook is saved before that point. - Accented characters were not tested. Our test code was plain ASCII. Export and import on PCs with the same Windows code page.
- Not for a server. Same rule as all Office automation: a desktop session with a person signed in.
Just want the scripts? The Scripts Pack · €12 has the code of this guide and the other guides ready to run, with examples and tests. The code on this page stays free.
The code on this page was written for this guide and tested on the versions above. The workbooks and their data are invented.