[Dev] Import d’un fichier Excel dans une liste SharePoint
Stack : Node.js + PnPjs v4 + xlsx + certificat Azure AD
Prérequis
- Projet SPFx existant avec PnPjs v4 configuré
- Certificat Azure AD en place (
private.key,thumbprint) - Liste SharePoint cible déjà créée avec les colonnes correspondantes
- Fichier
.envcomplété (voir guide de création de liste)
Phase 1 — Préparer le fichier Excel
Étape 1 — Respecter la structure attendue
La première ligne du fichier Excel doit contenir les noms exacts des colonnes de la liste SharePoint cible.
| Règle | Détail |
| Nom de colonne | Identique au nom interne SharePoint (sensible à la casse) |
| Dates | Format YYYY-MM-DD (ex. 2026-03-30) |
| Nombres | Valeurs numériques sans unité ni espace |
| Colonnes vides | Laisser la cellule vide — ne pas mettre N/A ou - |
| Colonne choix | La valeur doit correspondre exactement à l’un des choix définis |
Étape 2 — Placer le fichier dans le projet
mon-projet/
└── sessions.xlsx ← à la racine du projetPhase 2 — Installer la dépendance
Étape 3 — Installer la bibliothèque de lecture Excel
npm install xlsxPhase 3 — Créer le script d’import
Étape 4 — Créer scripts/importList.ts
import "dotenv/config";
import { spfi } from "@pnp/sp";
import { SPDefault } from "@pnp/nodejs";
import * as fs from "fs";
import * as path from "path";
import * as XLSX from "xlsx";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
// ─── Configuration ─────────────────────────────────────────────────
const TENANT_ID = process.env.TENANT_ID!;
const CLIENT_ID = process.env.CLIENT_ID!;
const THUMBPRINT = process.env.THUMBPRINT!;
const KEY_PATH = process.env.KEY_PATH!;
const SITE_URL = process.env.SITE_URL!;
const SP_ROOT_URL = process.env.SP_ROOT_URL!;
const LIST_NAME = "NomDeLaListe"; // ← à adapter
const EXCEL_PATH = path.resolve(__dirname, "../fichier.xlsx"); // ← à adapter
// ──────────────────────────────────────────────────────────────────
async function importData(): Promise<void> {
// Lecture du fichier Excel
const workbook = XLSX.readFile(EXCEL_PATH);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: Record<string, any>[] = XLSX.utils.sheet_to_json(sheet);
console.log(`${rows.length} ligne(s) trouvée(s).`);
// Connexion SharePoint
const privateKey = fs.readFileSync(KEY_PATH, "utf-8").trim();
const sp = spfi(SITE_URL).using(
SPDefault({
baseUrl: SITE_URL,
msal: {
config: {
auth: {
clientId: CLIENT_ID,
authority: `https://login.microsoftonline.com/${TENANT_ID}`,
clientCertificate: {
thumbprint: THUMBPRINT,
privateKey: privateKey
}
}
},
scopes: [`${SP_ROOT_URL}/.default`]
}
})
);
const list = sp.web.lists.getByTitle(LIST_NAME);
// Import ligne par ligne
for (const [index, row] of rows.entries()) {
try {
// Construire l'objet à envoyer — adapter les champs selon la liste
// ?? "" : envoyer une chaîne vide si la cellule est vide (colonnes texte)
// ?? null : envoyer null si la cellule est vide (colonnes date)
// Number() : convertir en nombre (colonnes numériques)
// Conversion de date JJ/MM/AAAA → AAAA-MM-JJ (format attendu par SharePoint)
// Exemple : "30/03/2026" → "2026-03-30"
const toIsoDate = (val: string | undefined): string | null => {
if (!val) return null;
const parts = val.split("/");
if (parts.length !== 3) return val; // déjà au bon format ou valeur inattendue
return `${parts[2]}-${parts[1]}-${parts[0]}`;
};
const item = {
Title: row["Title"] ?? "", // texte → chaîne vide par défaut
CodeFormation: row["CodeFormation"] ?? "", // texte → chaîne vide par défaut
DateSession: toIsoDate(row["DateSession"]), // JJ/MM/AAAA → AAAA-MM-JJ
NbConfirmes: Number(row["NbConfirmes"]) || 0, // nombre → 0 si vide ou NaN
NbOption: Number(row["NbOption"]) || 0, // nombre → 0 si vide ou NaN
// ↑ Ajouter ici tous les autres champs de la liste en suivant le même modèle
};
await list.items.add(item);
console.log(` [${index + 1}/${rows.length}] Importé : ${row["Title"] ?? row[Object.keys(row)[0]]}`);
} catch (err: any) {
console.error(` [${index + 1}] Erreur : ${err.message}`);
}
}
console.log("Import terminé.");
}
importData()
.then(() => process.exit(0))
.catch((err) => {
console.error("Erreur fatale :", err.message ?? err);
process.exit(1);
});Étape 5 — Adapter les types de colonnes
Dans la boucle d’import, identifier les colonnes nécessitant une conversion :
| Type SharePoint | Traitement dans le script |
| Ligne de texte | String(val) ou val ?? "" |
| Nombre | Number(val) || 0 |
| Date | Laisser en string YYYY-MM-DD — SharePoint accepte ce format |
| Choix | La valeur doit correspondre exactement à un choix existant |
| Oui/Non | Boolean(val) ou val === "Oui" |
| Personne | Requiert le loginName ou l’id utilisateur — ne pas passer en import direct |
Exemple concret de mapping avec les 3 types les plus courants
Lors de la construction de l’objet item à envoyer à SharePoint, chaque champ suit une logique différente selon son type :
// Colonne texte — si la cellule est vide, envoyer une chaîne vide plutôt que undefined
Title: row["Title"] ?? "",
// Colonne texte — même logique, le nom de la clé doit correspondre exactement
// au nom interne de la colonne SharePoint
CodeFormation: row["CodeFormation"] ?? "",
// Colonne date — si la cellule est vide, envoyer null plutôt qu'une chaîne vide
// (SharePoint rejetterait "" pour un champ de type date)
DateSession: row["DateSession"] ?? null,L’opérateur ?? (nullish coalescing) renvoie la valeur de droite uniquement si la valeur de gauche est null ou undefined — il n’écrase pas 0 ou false, contrairement à ||.
Phase 4 — Configurer et lancer
Étape 6 — Ajouter le script dans package.json
"scripts": {
"import-list": "ts-node --project tsconfig.scripts.json scripts/importList.ts"
}Étape 7 — Vérifier la structure du projet
mon-projet/
├── fichier.xlsx ← fichier Excel source
├── scripts/
│ └── importList.ts ← script d'import
├── .env ← variables d'environnement
├── tsconfig.scripts.json
└── package.jsonÉtape 8 — Lancer l’import
npm run import-listRésultat attendu :
N ligne(s) trouvée(s).
[1/N] Importé : ...
[2/N] Importé : ...
...
Import terminé.Gestion des erreurs courantes
| Erreur | Cause probable | Correction |
| 401 Unauthorized | Certificat ou permissions incorrects | Vérifier Azure AD et le thumbprint |
| Unsupported app only token | Permission SharePoint de type Application manquante | Ajouter Sites.FullControl.All Application dans Azure AD |
| The field X does not exist | Nom de colonne Excel différent du nom interne SP | Vérifier la casse et le nom exact de la colonne |
| Invalid choice value | Valeur non définie dans la colonne Choix | Vérifier les valeurs autorisées dans la liste SP |
| Cannot convert to number | Cellule vide dans une colonne numérique | Ajouter ` |
Bonnes pratiques
- Tester l’import avec une seule ligne avant de lancer les données complètes
- Conserver le fichier Excel source dans Git (sans données sensibles)
- Ne jamais commiter
.env,private.keyetappcert.pfx - En cas d’import partiel (erreur à mi-chemin), supprimer les éléments déjà créés avant de relancer pour éviter les doublons
- Pour les gros volumes (> 100 lignes), envisager un import par batch avec
Promise.all()par groupes de 10