microsoft / microsoft/vscode-azureresourcegroups

Scaffolded Functions project builds green but cannot start: tsconfig path alias emits an unresolvable runtime specifier

Open
#1,757 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
78
Forks
55
Avg merge
16h 49m
Merged PRs (30d)
139

Description

For a coding agent: the fix is in this repo, in the scaffold agent instructions — not in any generated project. Jump to Where to fix it.

Summary

The scaffold agent emits a monorepo whose Functions workspace imports shared code through a tsconfig paths alias (@shared/*). TypeScript resolves that at compile time and does not rewrite module specifiers on emit, so the emitted JavaScript still asks Node for a package that does not exist. The project builds clean, type-checks clean, and the Functions worker dies on load.

This violates a rule the product documentation already states — it is simply not enforced anywhere.

Evidence

Found by the MSBench eval suite in run 2026083057881445 (stack react-functions-postgres, phase local).

validate-project-builds passes:

[project-builds] services/functions: npm ci
[project-builds] services/functions: npm run build
[project-builds] built 3 package(s): services/functions, services/shared, services/web
PASS: gate=project-builds — scaffolded project installs and builds

The app then fails to start:

Azure Functions Core Tools
Core Tools Version:       4.14.0+4a17060ecc915f1672d86717a487ace30f535e74 (64-bit)
Function Runtime Version: 4.1052.200.26352

[...] Worker was unable to load entry point "dist/functions/src/functions/createTask.js":
      Cannot find module '@shared/schemas/index'
Require stack:
- /workspace/services/functions/dist/functions/src/functions/createTask.js
- /usr/lib/azure-functions-core-tools-4/workers/node/dist/src/worker-bundle.js

All five runtime-* gates fail as a cascade of this one root cause.

Root cause

The generated project contains two competing mechanisms for the same shared code.

services/shared/package.json is correct and well-formed, with real subpath exports:

{
  "name": "@task-tracker/shared",
  "type": "module",
  "main": "./dist/index.js",
  "exports": {
    ".":         { "types": "./dist/index.d.ts",         "default": "./dist/index.js" },
    "./types":   { "types": "./dist/types/index.d.ts",   "default": "./dist/types/index.js" },
    "./schemas": { "types": "./dist/schemas/index.d.ts", "default": "./dist/schemas/index.js" }
  }
}

But the Functions code never imports that package. It goes through an alias pointing at the shared package's source:

// services/functions/tsconfig.json
"paths": { "@shared/*": ["shared/src/*"] }
import { createTaskSchema } from '@shared/schemas/index';
import type { Task, CreateTaskDto } from '@shared/types/index';

tsc resolves @shared/* and emits require('@shared/schemas/index') verbatim. At runtime there is no @shared package in node_modules and no runtime path resolver — no tsconfig-paths, no bundler, no imports map — so Node cannot resolve it.

The correct specifier for the package that was actually generated is @task-tracker/shared/schemas; the documented convention is the relative form ../shared/....

The rule this breaks already exists

resources/agents/shared-references/architecture.md states it twice, in the naming callout:

Whatever names you pick, apply them consistently everywhere — npm workspaces, cd commands, tsconfig rootDir, and the computed main field […] Imports of the shared package stay ../shared/....

and prescribes the supported mechanism under TypeScript Cross-Workspace Import Configuration:

When Functions imports from ../shared/, tsconfig.json must set rootDir to reach outside workspace

// services/functions/tsconfig.json
{
  "compilerOptions": { "rootDir": "..", "outDir": "dist" },
  "include": ["src/**/*.ts", "../shared/**/*.ts"]
}

The generated project did follow the rootDir half — the emitted path is dist/functions/src/functions/createTask.js, exactly the documented rootDir: ".." shape — and then imported by alias anyway. Every code example in the instructions uses relative imports ('../../services/shared/types/entities'). No file in resources/agents/ mentions paths or @shared at all, so the alias is invented by the model, and nothing tells it not to.

Why the existing checkpoints miss it

resources/agents/azure-project-scaffold/instructions.md, Step 2 checkpoint block:

# Checkpoint Why it passes anyway
3 Shared package has exports/main, builds to dist/ True here. The package is fine; nothing imports it.
4 Cross-workspace imports (CRITICAL): run tsc --noEmit, fix TS2307 The alias is precisely what makes tsc succeed. This check can never fire on this defect.
5 rootDir/main match actual dist/ output True here.
6 Production install must satisfy every import in dist/ Closest in spirit, but framed as a dependencies/devDependencies split, so it reads as "is the package in the right section" rather than "is this specifier resolvable at all".

Checkpoint 4 is the one that looks like it covers this and does not. Every check is a compile-time check, and this is a defect that only exists after emit.

Where to fix it

1. resources/agents/azure-project-scaffold/instructions.md — Step 2 checkpoint block (the ✅ Checkpoint list containing items 1–6 and the ⚠️ Pitfalls line).

Add a checkpoint that inspects emitted output, not source. Suggested wording:

Emitted specifiers resolve (CRITICAL): tsc does not rewrite module specifiers. Any non-relative import that only resolved through a tsconfig paths alias will still be in dist/ and will fail at runtime with Cannot find module. After building, verify every non-relative specifier in dist/ resolves from that workspace. Do not use paths aliases for cross-workspace imports — import the shared package by its name, or use the relative ../shared/... form.

Add to the ⚠️ Pitfalls line: paths aliases resolve at compile time only → Cannot find module at runtime.

2. resources/agents/shared-references/architecture.mdTypeScript Cross-Workspace Import Configuration section.

The "imports stay ../shared/..." rule currently lives inside a callout about folder naming, which is not where anyone looks for import mechanics. State it in this section as a rule with its reason, and show the alias as an explicit anti-pattern:

// ❌ compiles, fails at runtime — tsc emits require('@shared/schemas/index') unchanged
{ "compilerOptions": { "paths": { "@shared/*": ["shared/src/*"] } } }

Non-goal: do not add tsconfig-paths or a bundler to make the alias work. The supported mechanisms are the relative import and the workspace package name; adding a third would leave the same two competing mechanisms in place.

Verification command

Cheap, dependency-free, and would have caught this. Run from the workspace that emitted dist/:

node -e "
const fs=require('fs'),path=require('path'),Module=require('module');
const bad=[];
(function walk(d){ for(const e of fs.readdirSync(d,{withFileTypes:true})){
  const p=path.join(d,e.name);
  if(e.isDirectory()) walk(p);
  else if(e.name.endsWith('.js')) for(const m of fs.readFileSync(p,'utf8').matchAll(/require\(['\\\"]([^'\\\"]+)['\\\"]\)/g)){
    const s=m[1];
    if(s.startsWith('.')||s.startsWith('node:')||Module.builtinModules.includes(s)) continue;
    try { Module.createRequire(p).resolve(s); } catch { bad.push(p+' -> '+s); }
  }}})('dist');
if(bad.length){ console.error('unresolvable specifiers in emitted output:'); bad.forEach(b=>console.error('  '+b)); process.exit(1); }
console.log('all emitted specifiers resolve');
"

On the run above this prints dist/functions/src/functions/createTask.js -> @shared/schemas/index and exits 1.

How to confirm the fix

The eval suite reproduces this end to end. From evals/:

BENCHMARK=corbench.cor_functions_host bash msbench/run.sh --skip-build \
  --stack react-functions-postgres --phase local \
  --dataset evals/msbench/container/dataset.jsonl

npm run analyze-run -- <run-id>      # per-gate verdicts

Fixed looks like runtime-app-starts passing and runtime-crud completing a round-trip. The datastore emulators are already running in this image, so a failure there will be about the app, not the environment — gates-selftest-emulators proves the emulator path independently (run 2026083068733550, runtime-crud green against a preamble-started PostgreSQL).

Not the cause

The environment was healthy for the run that found this:

func already present: 4.14.0
azurite: listening on 127.0.0.1:10000
postgres: ready on 127.0.0.1:5432 (role taskuser, db tasktracker)

Run link: https://msbenchapp.azurewebsites.net/run-analysis/2026083057881445 — verified as a genuine result (156 /v1/messages calls, claude-sonnet-4.5 requested and active, not throttled).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Step 2 checkpoint block in resources/agents/azure-project-scaffold/instructions.md and the TypeScript Cross-Workspace Import Configuration section in resources/agents/shared-references/architecture.md. Review the emitted-specifier verification command, then update both documents to explain the runtime limitation and supported import forms. Done means the generated project passes runtime-app-starts and runtime-crud, with no unresolved specifiers in dist/.

Written by the indexing model from the issue text.

Assessment

Tech stack
azure, node.js, typescript
Domain
documentation
Issue type
Documentation
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.