Skip to content

[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 .env complé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
There are no rows in this table

Étape 2 — Placer le fichier dans le projet

mon-projet/
└── sessions.xlsx ← à la racine du projet

Phase 2 — Installer la dépendance

Étape 3 — Installer la bibliothèque de lecture Excel

npm install xlsx

Phase 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
There are no rows in this table
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-list
Ré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 `
There are no rows in this table

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.key et appcert.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
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.